Compare commits
34 Commits
master
...
feat/mobil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3de3827103 | ||
|
|
db2fc41a67 | ||
|
|
c471ca7bd0 | ||
|
|
4f37f9d928 | ||
|
|
c8f22bda95 | ||
|
|
bfe2c36756 | ||
|
|
092fdec8a3 | ||
|
|
0e69f59c44 | ||
|
|
894d67d426 | ||
|
|
e8cdf48771 | ||
|
|
01ccea9542 | ||
|
|
3c2708c9d7 | ||
|
|
6b9e4072cb | ||
|
|
a870bd12b9 | ||
|
|
192a3bb12b | ||
|
|
7dfbab5ee9 | ||
|
|
a4f658df02 | ||
|
|
37453b4f4b | ||
|
|
debca1370f | ||
|
|
55316b1175 | ||
|
|
fd17bb17a4 | ||
|
|
5b86b0e7f4 | ||
|
|
394b29b244 | ||
|
|
9c3111850d | ||
|
|
4ece9d6efe | ||
|
|
5b0ec76a8a | ||
|
|
a376a25ff4 | ||
|
|
3b3d5a156a | ||
|
|
de42e142a3 | ||
|
|
a3207f81be | ||
|
|
05138f2116 | ||
|
|
88a3a3a000 | ||
|
|
3553576bae | ||
|
|
40db445b1a |
2
index.html
vendored
2
index.html
vendored
@@ -5,7 +5,7 @@
|
||||
<title>HuLa</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no viewport-fit=cover" />
|
||||
content="width=device-width, initial-scale=1, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
|
||||
<!-- 🎯 预加载图片资源(仅移动端) -->
|
||||
<script>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
RUST_BACKTRACE=1
|
||||
# APP_ENVIRONMENT=local
|
||||
APP_ENVIRONMENT=local
|
||||
APP_ENVIRONMENT=production
|
||||
|
||||
89
src-tauri/Cargo.lock
generated
89
src-tauri/Cargo.lock
generated
@@ -4317,6 +4317,16 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-javascript-core"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9052cb1bb50a4c161d934befcf879526fb87ae9a68858f241e693ca46225cf5a"
|
||||
dependencies = [
|
||||
"objc2 0.6.2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-metal"
|
||||
version = "0.2.2"
|
||||
@@ -4365,6 +4375,17 @@ dependencies = [
|
||||
"objc2-foundation 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-security"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1f8e0ef3ab66b08c42644dcb34dba6ec0a574bbd8adbb8bdbdc7a2779731a44"
|
||||
dependencies = [
|
||||
"bitflags 2.9.2",
|
||||
"objc2 0.6.2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-ui-kit"
|
||||
version = "0.3.1"
|
||||
@@ -4389,6 +4410,8 @@ dependencies = [
|
||||
"objc2-app-kit 0.3.1",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.1",
|
||||
"objc2-javascript-core",
|
||||
"objc2-security",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6260,9 +6283,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serialize-to-javascript"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb"
|
||||
checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -6271,13 +6294,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serialize-to-javascript-impl"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763"
|
||||
checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7068,12 +7091,13 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "2.7.0"
|
||||
version = "2.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "352a4bc7bf6c25f5624227e3641adf475a6535707451b09bb83271df8b7a6ac7"
|
||||
checksum = "5d545ccf7b60dcd44e07c6fb5aeb09140966f0aabd5d2aa14a6821df7bc99348"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"cookie",
|
||||
"dirs 6.0.0",
|
||||
"dunce",
|
||||
"embed_plist",
|
||||
@@ -7093,6 +7117,7 @@ dependencies = [
|
||||
"objc2-app-kit 0.3.1",
|
||||
"objc2-foundation 0.3.1",
|
||||
"objc2-ui-kit",
|
||||
"objc2-web-kit",
|
||||
"percent-encoding",
|
||||
"plist",
|
||||
"raw-window-handle",
|
||||
@@ -7120,9 +7145,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "2.3.1"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "182d688496c06bf08ea896459bf483eb29cdff35c1c4c115fb14053514303064"
|
||||
checksum = "67945dbaf8920dbe3a1e56721a419a0c3d085254ab24cff5b9ad55e2b0016e0b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
@@ -7136,15 +7161,15 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"tauri-winres",
|
||||
"toml 0.8.2",
|
||||
"toml 0.9.5",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "2.3.1"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b54a99a6cd8e01abcfa61508177e6096a4fe2681efecee9214e962f2f073ae4a"
|
||||
checksum = "1ab3a62cf2e6253936a8b267c2e95839674e7439f104fa96ad0025e149d54d8a"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"brotli",
|
||||
@@ -7169,9 +7194,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "2.3.2"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7945b14dc45e23532f2ded6e120170bbdd4af5ceaa45784a6b33d250fbce3f9e"
|
||||
checksum = "4368ea8094e7045217edb690f493b55b30caf9f3e61f79b4c24b6db91f07995e"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@@ -7183,9 +7208,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin"
|
||||
version = "2.3.1"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bd5c1e56990c70a906ef67a9851bbdba9136d26075ee9a2b19c8b46986b3e02"
|
||||
checksum = "9946a3cede302eac0c6eb6c6070ac47b1768e326092d32efbb91f21ed58d978f"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"glob",
|
||||
@@ -7194,7 +7219,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"toml 0.8.2",
|
||||
"toml 0.9.5",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -7411,9 +7436,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-os"
|
||||
version = "2.3.0"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05bccb4c6de4299beec5a9b070878a01bce9e2c945aa7a75bcea38bcba4c675d"
|
||||
checksum = "77a1c77ebf6f20417ab2a74e8c310820ba52151406d0c80fbcea7df232e3f6ba"
|
||||
dependencies = [
|
||||
"gethostname 1.0.2",
|
||||
"log",
|
||||
@@ -7472,9 +7497,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-single-instance"
|
||||
version = "2.3.2"
|
||||
version = "2.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50a0e5a4ce43cb3a733c3aef85e8478bc769dac743c615e26639cbf5d953faf7"
|
||||
checksum = "236043404a4d1502ed7cce11a8ec88ea1e85597eec9887b4701bb10b66b13b6e"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -7557,9 +7582,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.7.1"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b1cc885be806ea15ff7b0eb47098a7b16323d9228876afda329e34e2d6c4676"
|
||||
checksum = "d4cfc9ad45b487d3fded5a4731a567872a4812e9552e3964161b08edabf93846"
|
||||
dependencies = [
|
||||
"cookie",
|
||||
"dpi",
|
||||
@@ -7568,20 +7593,23 @@ dependencies = [
|
||||
"jni",
|
||||
"objc2 0.6.2",
|
||||
"objc2-ui-kit",
|
||||
"objc2-web-kit",
|
||||
"raw-window-handle",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.15",
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "2.7.2"
|
||||
version = "2.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe653a2fbbef19fe898efc774bc52c8742576342a33d3d028c189b57eb1d2439"
|
||||
checksum = "c1fe9d48bd122ff002064e88cfcd7027090d789c4302714e68fcccba0f4b7807"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http 1.3.1",
|
||||
@@ -7606,9 +7634,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "2.6.0"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330c15cabfe1d9f213478c9e8ec2b0c76dab26bb6f314b8ad1c8a568c1d186e"
|
||||
checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"brotli",
|
||||
@@ -7635,7 +7663,7 @@ dependencies = [
|
||||
"serde_with",
|
||||
"swift-rs",
|
||||
"thiserror 2.0.15",
|
||||
"toml 0.8.2",
|
||||
"toml 0.9.5",
|
||||
"url",
|
||||
"urlpattern",
|
||||
"uuid",
|
||||
@@ -9590,14 +9618,15 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
|
||||
|
||||
[[package]]
|
||||
name = "wry"
|
||||
version = "0.52.1"
|
||||
version = "0.53.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12a714d9ba7075aae04a6e50229d6109e3d584774b99a6a8c60de1698ca111b9"
|
||||
checksum = "31f0e9642a0d061f6236c54ccae64c2722a7879ad4ec7dff59bd376d446d8e90"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"block2 0.6.1",
|
||||
"cookie",
|
||||
"crossbeam-channel",
|
||||
"dirs 6.0.0",
|
||||
"dpi",
|
||||
"dunce",
|
||||
"gdkx11",
|
||||
|
||||
@@ -31,11 +31,11 @@ name = "hula_app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.3.1", features = [] }
|
||||
tauri-build = { version = "2.4.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
# Tauri 官方依赖
|
||||
tauri = { version = "2.7.0", features = [
|
||||
tauri = { version = "2.8.4", features = [
|
||||
"protocol-asset",
|
||||
"macos-private-api",
|
||||
"tray-icon",
|
||||
@@ -109,7 +109,7 @@ tauri-plugin-barcode-scanner = "2.4.0"
|
||||
# 不兼容移动端的依赖
|
||||
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
|
||||
tauri-plugin-autostart = "2"
|
||||
tauri-plugin-single-instance = "2.3.2"
|
||||
tauri-plugin-single-instance = "2.3.3"
|
||||
tauri-plugin-updater = "2"
|
||||
screenshots = "0.8.10"
|
||||
|
||||
|
||||
@@ -10,5 +10,14 @@
|
||||
<string>HuLa needs screen recording permission to enable screenshot functionality</string>
|
||||
<key>NSAccessibilityUsageDescription</key>
|
||||
<string>HuLa needs accessibility permission to capture screen content</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
android:label="@string/main_activity_title"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:resizeableActivity="true"
|
||||
android:windowSoftInputMode="adjustNothing"
|
||||
android:theme="@style/Theme.hula">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -1,130 +1,79 @@
|
||||
package com.hula_ios.app
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebView
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import android.graphics.Rect
|
||||
import android.view.ViewTreeObserver
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class MainActivity : TauriActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Enable edge-to-edge display for SafeArea support
|
||||
// 全屏 Edge-to-Edge
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
}
|
||||
|
||||
override fun onWebViewCreate(webView: WebView) {
|
||||
super.onWebViewCreate(webView)
|
||||
|
||||
// Apply window insets for SafeArea support
|
||||
// 初始化 WebView 背景填充
|
||||
// webView.setBackgroundColor(0x00000000) // 透明,父布局背景可见
|
||||
|
||||
// 监听安全区 Insets 并注入 CSS 变量
|
||||
ViewCompat.setOnApplyWindowInsetsListener(webView) { _, insets ->
|
||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val displayCutout = insets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||
|
||||
// Calculate safe area insets
|
||||
val safeAreaTop = maxOf(systemBars.top, displayCutout.top)
|
||||
val safeAreaBottom = maxOf(systemBars.bottom, displayCutout.bottom)
|
||||
val safeAreaLeft = maxOf(systemBars.left, displayCutout.left)
|
||||
val safeAreaRight = maxOf(systemBars.right, displayCutout.right)
|
||||
|
||||
// Convert dp to px for CSS
|
||||
val density = resources.displayMetrics.density
|
||||
val topPx = (safeAreaTop / density).toInt()
|
||||
val bottomPx = (safeAreaBottom / density).toInt()
|
||||
val leftPx = (safeAreaLeft / density).toInt()
|
||||
val rightPx = (safeAreaRight / density).toInt()
|
||||
|
||||
// Inject CSS custom properties for safe area insets
|
||||
val safeTop = maxOf(systemBars.top, displayCutout.top)
|
||||
val safeBottom = maxOf(systemBars.bottom, displayCutout.bottom)
|
||||
val safeLeft = maxOf(systemBars.left, displayCutout.left)
|
||||
val safeRight = maxOf(systemBars.right, displayCutout.right)
|
||||
|
||||
val script = """
|
||||
document.documentElement.style.setProperty('--safe-area-inset-top', '${topPx}px');
|
||||
document.documentElement.style.setProperty('--safe-area-inset-bottom', '${bottomPx}px');
|
||||
document.documentElement.style.setProperty('--safe-area-inset-left', '${leftPx}px');
|
||||
document.documentElement.style.setProperty('--safe-area-inset-right', '${rightPx}px');
|
||||
|
||||
if (CSS && CSS.registerProperty) {
|
||||
try {
|
||||
CSS.registerProperty({ name: '--safe-area-inset-top', syntax: '<length>', inherits: true, initialValue: '${topPx}px' });
|
||||
CSS.registerProperty({ name: '--safe-area-inset-bottom', syntax: '<length>', inherits: true, initialValue: '${bottomPx}px' });
|
||||
CSS.registerProperty({ name: '--safe-area-inset-left', syntax: '<length>', inherits: true, initialValue: '${leftPx}px' });
|
||||
CSS.registerProperty({ name: '--safe-area-inset-right', syntax: '<length>', inherits: true, initialValue: '${rightPx}px' });
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
window.dispatchEvent(new CustomEvent('safeAreaChanged', {
|
||||
detail: { top: ${topPx}, bottom: ${bottomPx}, left: ${leftPx}, right: ${rightPx} }
|
||||
}));
|
||||
document.documentElement.style.setProperty('--safe-area-inset-top', '${(safeTop/density).roundToInt()}px');
|
||||
document.documentElement.style.setProperty('--safe-area-inset-bottom', '${(safeBottom/density).roundToInt()}px');
|
||||
document.documentElement.style.setProperty('--safe-area-inset-left', '${(safeLeft/density).roundToInt()}px');
|
||||
document.documentElement.style.setProperty('--safe-area-inset-right', '${(safeRight/density).roundToInt()}px');
|
||||
""".trimIndent()
|
||||
|
||||
webView.evaluateJavascript(script, null)
|
||||
insets
|
||||
}
|
||||
|
||||
// Add keyboard visibility listener
|
||||
webView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
private var lastHeightDiff = 0
|
||||
private var isKeyboardVisible = false
|
||||
// 监听键盘弹出,并注入 CSS 变量 + 触发 JS 事件
|
||||
ViewCompat.setOnApplyWindowInsetsListener(webView) { _, insets ->
|
||||
val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
||||
val density = resources.displayMetrics.density
|
||||
val heightDp = (imeHeight / density).roundToInt()
|
||||
|
||||
override fun onGlobalLayout() {
|
||||
val rect = Rect()
|
||||
webView.getWindowVisibleDisplayFrame(rect)
|
||||
|
||||
val screenHeightPx = webView.rootView.height
|
||||
val visibleHeightPx = rect.bottom
|
||||
val heightDiffPx = screenHeightPx - visibleHeightPx
|
||||
|
||||
val density = webView.resources.displayMetrics.density
|
||||
|
||||
val screenHeight = (screenHeightPx / density).roundToInt()
|
||||
val visibleHeight = (visibleHeightPx / density).roundToInt()
|
||||
val heightDiff = (heightDiffPx / density).roundToInt()
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
if (heightDiffPx > screenHeightPx * 0.15) {
|
||||
if (!isKeyboardVisible || heightDiffPx != lastHeightDiff) {
|
||||
isKeyboardVisible = true
|
||||
lastHeightDiff = heightDiffPx
|
||||
|
||||
val script = """
|
||||
window.dispatchEvent(new CustomEvent('keyboardDidShow', {
|
||||
detail: {
|
||||
height: $heightDiff,
|
||||
visibleHeight: $visibleHeight,
|
||||
screenHeight: $screenHeight,
|
||||
bottomInset: $heightDiff,
|
||||
timestamp: $now,
|
||||
keyboardVisible: true
|
||||
}
|
||||
}));
|
||||
""".trimIndent()
|
||||
|
||||
webView.evaluateJavascript(script, null)
|
||||
}
|
||||
} else {
|
||||
if (isKeyboardVisible) {
|
||||
isKeyboardVisible = false
|
||||
|
||||
val script = """
|
||||
window.dispatchEvent(new CustomEvent('keyboardDidHide', {
|
||||
detail: {
|
||||
height: 0,
|
||||
visibleHeight: $visibleHeight,
|
||||
screenHeight: $screenHeight,
|
||||
bottomInset: 0,
|
||||
timestamp: $now,
|
||||
keyboardVisible: false
|
||||
}
|
||||
}));
|
||||
""".trimIndent()
|
||||
|
||||
webView.evaluateJavascript(script, null)
|
||||
}
|
||||
}
|
||||
if (imeHeight > 0) {
|
||||
// 键盘显示
|
||||
val script = """
|
||||
document.documentElement.style.setProperty('--keyboard-height', '${heightDp}px');
|
||||
window.dispatchEvent(new CustomEvent('keyboardDidShow', {
|
||||
detail: { height: $heightDp }
|
||||
}));
|
||||
""".trimIndent()
|
||||
webView.evaluateJavascript(script, null)
|
||||
} else {
|
||||
// 键盘隐藏
|
||||
val script = """
|
||||
document.documentElement.style.setProperty('--keyboard-height', '0px');
|
||||
window.dispatchEvent(new CustomEvent('keyboardDidHide', {
|
||||
detail: { height: 0 }
|
||||
}));
|
||||
""".trimIndent()
|
||||
webView.evaluateJavascript(script, null)
|
||||
}
|
||||
})
|
||||
|
||||
insets
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
src-tauri/gen/apple/hula_iOS/Info.plist
vendored
6
src-tauri/gen/apple/hula_iOS/Info.plist
vendored
@@ -30,15 +30,11 @@
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Request camera access for WebRTC</string>
|
||||
@@ -48,7 +44,5 @@
|
||||
<string>HuLa needs screen recording permission to enable screenshot functionality</string>
|
||||
<key>NSAccessibilityUsageDescription</key>
|
||||
<string>HuLa needs accessibility permission to capture screen content</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>HuLa使用摄像头</string>
|
||||
</dict>
|
||||
</plist>
|
||||
2
src-tauri/gen/schemas/acl-manifests.json
vendored
2
src-tauri/gen/schemas/acl-manifests.json
vendored
File diff suppressed because one or more lines are too long
24
src-tauri/gen/schemas/android-schema.json
vendored
24
src-tauri/gen/schemas/android-schema.json
vendored
@@ -3950,6 +3950,12 @@
|
||||
"const": "core:window:allow-set-focus",
|
||||
"markdownDescription": "Enables the set_focus command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_focusable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:allow-set-focusable",
|
||||
"markdownDescription": "Enables the set_focusable command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -4022,6 +4028,12 @@
|
||||
"const": "core:window:allow-set-shadow",
|
||||
"markdownDescription": "Enables the set_shadow command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_simple_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:allow-set-simple-fullscreen",
|
||||
"markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_size command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -4394,6 +4406,12 @@
|
||||
"const": "core:window:deny-set-focus",
|
||||
"markdownDescription": "Denies the set_focus command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_focusable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:deny-set-focusable",
|
||||
"markdownDescription": "Denies the set_focusable command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -4466,6 +4484,12 @@
|
||||
"const": "core:window:deny-set-shadow",
|
||||
"markdownDescription": "Denies the set_shadow command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_simple_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:deny-set-simple-fullscreen",
|
||||
"markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_size command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
|
||||
24
src-tauri/gen/schemas/desktop-schema.json
vendored
24
src-tauri/gen/schemas/desktop-schema.json
vendored
@@ -3914,6 +3914,12 @@
|
||||
"const": "core:window:allow-set-focus",
|
||||
"markdownDescription": "Enables the set_focus command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_focusable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:allow-set-focusable",
|
||||
"markdownDescription": "Enables the set_focusable command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -3986,6 +3992,12 @@
|
||||
"const": "core:window:allow-set-shadow",
|
||||
"markdownDescription": "Enables the set_shadow command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_simple_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:allow-set-simple-fullscreen",
|
||||
"markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_size command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -4358,6 +4370,12 @@
|
||||
"const": "core:window:deny-set-focus",
|
||||
"markdownDescription": "Denies the set_focus command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_focusable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:deny-set-focusable",
|
||||
"markdownDescription": "Denies the set_focusable command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -4430,6 +4448,12 @@
|
||||
"const": "core:window:deny-set-shadow",
|
||||
"markdownDescription": "Denies the set_shadow command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_simple_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:deny-set-simple-fullscreen",
|
||||
"markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_size command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
|
||||
102
src-tauri/gen/schemas/iOS-schema.json
vendored
102
src-tauri/gen/schemas/iOS-schema.json
vendored
@@ -2480,6 +2480,84 @@
|
||||
"Identifier": {
|
||||
"description": "Permission identifier",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "This permission set configures which\nbarcode scanning features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all barcode related features.\n\n\n#### This default permission set includes:\n\n- `allow-cancel`\n- `allow-check-permissions`\n- `allow-open-app-settings`\n- `allow-request-permissions`\n- `allow-scan`\n- `allow-vibrate`",
|
||||
"type": "string",
|
||||
"const": "barcode-scanner:default",
|
||||
"markdownDescription": "This permission set configures which\nbarcode scanning features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all barcode related features.\n\n\n#### This default permission set includes:\n\n- `allow-cancel`\n- `allow-check-permissions`\n- `allow-open-app-settings`\n- `allow-request-permissions`\n- `allow-scan`\n- `allow-vibrate`"
|
||||
},
|
||||
{
|
||||
"description": "Enables the cancel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "barcode-scanner:allow-cancel",
|
||||
"markdownDescription": "Enables the cancel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the check_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "barcode-scanner:allow-check-permissions",
|
||||
"markdownDescription": "Enables the check_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the open_app_settings command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "barcode-scanner:allow-open-app-settings",
|
||||
"markdownDescription": "Enables the open_app_settings command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the request_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "barcode-scanner:allow-request-permissions",
|
||||
"markdownDescription": "Enables the request_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the scan command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "barcode-scanner:allow-scan",
|
||||
"markdownDescription": "Enables the scan command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the vibrate command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "barcode-scanner:allow-vibrate",
|
||||
"markdownDescription": "Enables the vibrate command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the cancel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "barcode-scanner:deny-cancel",
|
||||
"markdownDescription": "Denies the cancel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the check_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "barcode-scanner:deny-check-permissions",
|
||||
"markdownDescription": "Denies the check_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the open_app_settings command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "barcode-scanner:deny-open-app-settings",
|
||||
"markdownDescription": "Denies the open_app_settings command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the request_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "barcode-scanner:deny-request-permissions",
|
||||
"markdownDescription": "Denies the request_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the scan command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "barcode-scanner:deny-scan",
|
||||
"markdownDescription": "Denies the scan command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the vibrate command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "barcode-scanner:deny-vibrate",
|
||||
"markdownDescription": "Denies the vibrate command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "No features are enabled by default, as we believe\nthe clipboard can be inherently dangerous and it is \napplication specific if read and/or write access is needed.\n\nClipboard interaction needs to be explicitly enabled.\n",
|
||||
"type": "string",
|
||||
@@ -3872,6 +3950,12 @@
|
||||
"const": "core:window:allow-set-focus",
|
||||
"markdownDescription": "Enables the set_focus command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_focusable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:allow-set-focusable",
|
||||
"markdownDescription": "Enables the set_focusable command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -3944,6 +4028,12 @@
|
||||
"const": "core:window:allow-set-shadow",
|
||||
"markdownDescription": "Enables the set_shadow command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_simple_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:allow-set-simple-fullscreen",
|
||||
"markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_size command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -4316,6 +4406,12 @@
|
||||
"const": "core:window:deny-set-focus",
|
||||
"markdownDescription": "Denies the set_focus command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_focusable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:deny-set-focusable",
|
||||
"markdownDescription": "Denies the set_focusable command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -4388,6 +4484,12 @@
|
||||
"const": "core:window:deny-set-shadow",
|
||||
"markdownDescription": "Denies the set_shadow command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_simple_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:deny-set-simple-fullscreen",
|
||||
"markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_size command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
|
||||
24
src-tauri/gen/schemas/macOS-schema.json
vendored
24
src-tauri/gen/schemas/macOS-schema.json
vendored
@@ -3914,6 +3914,12 @@
|
||||
"const": "core:window:allow-set-focus",
|
||||
"markdownDescription": "Enables the set_focus command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_focusable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:allow-set-focusable",
|
||||
"markdownDescription": "Enables the set_focusable command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -3986,6 +3992,12 @@
|
||||
"const": "core:window:allow-set-shadow",
|
||||
"markdownDescription": "Enables the set_shadow command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_simple_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:allow-set-simple-fullscreen",
|
||||
"markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_size command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -4358,6 +4370,12 @@
|
||||
"const": "core:window:deny-set-focus",
|
||||
"markdownDescription": "Denies the set_focus command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_focusable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:deny-set-focusable",
|
||||
"markdownDescription": "Denies the set_focusable command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -4430,6 +4448,12 @@
|
||||
"const": "core:window:deny-set-shadow",
|
||||
"markdownDescription": "Denies the set_shadow command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_simple_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:deny-set-simple-fullscreen",
|
||||
"markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_size command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
|
||||
24
src-tauri/gen/schemas/mobile-schema.json
vendored
24
src-tauri/gen/schemas/mobile-schema.json
vendored
@@ -3950,6 +3950,12 @@
|
||||
"const": "core:window:allow-set-focus",
|
||||
"markdownDescription": "Enables the set_focus command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_focusable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:allow-set-focusable",
|
||||
"markdownDescription": "Enables the set_focusable command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -4022,6 +4028,12 @@
|
||||
"const": "core:window:allow-set-shadow",
|
||||
"markdownDescription": "Enables the set_shadow command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_simple_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:allow-set-simple-fullscreen",
|
||||
"markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_size command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -4394,6 +4406,12 @@
|
||||
"const": "core:window:deny-set-focus",
|
||||
"markdownDescription": "Denies the set_focus command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_focusable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:deny-set-focusable",
|
||||
"markdownDescription": "Denies the set_focusable command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -4466,6 +4484,12 @@
|
||||
"const": "core:window:deny-set-shadow",
|
||||
"markdownDescription": "Denies the set_shadow command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_simple_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:deny-set-simple-fullscreen",
|
||||
"markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_size command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
|
||||
24
src-tauri/gen/schemas/windows-schema.json
vendored
24
src-tauri/gen/schemas/windows-schema.json
vendored
@@ -3914,6 +3914,12 @@
|
||||
"const": "core:window:allow-set-focus",
|
||||
"markdownDescription": "Enables the set_focus command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_focusable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:allow-set-focusable",
|
||||
"markdownDescription": "Enables the set_focusable command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -3986,6 +3992,12 @@
|
||||
"const": "core:window:allow-set-shadow",
|
||||
"markdownDescription": "Enables the set_shadow command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_simple_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:allow-set-simple-fullscreen",
|
||||
"markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_size command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -4358,6 +4370,12 @@
|
||||
"const": "core:window:deny-set-focus",
|
||||
"markdownDescription": "Denies the set_focus command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_focusable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:deny-set-focusable",
|
||||
"markdownDescription": "Denies the set_focusable command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -4430,6 +4448,12 @@
|
||||
"const": "core:window:deny-set-shadow",
|
||||
"markdownDescription": "Denies the set_shadow command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_simple_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:deny-set-simple-fullscreen",
|
||||
"markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_size command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
|
||||
6
src-tauri/tauri.android.conf.json
vendored
6
src-tauri/tauri.android.conf.json
vendored
@@ -11,9 +11,7 @@
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"resources": [
|
||||
"configuration"
|
||||
],
|
||||
"resources": ["configuration"],
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
@@ -41,7 +39,7 @@
|
||||
"windows": [
|
||||
{
|
||||
"title": "登录",
|
||||
"label": "login",
|
||||
"label": "mobile-home",
|
||||
"url": "/mobile/login",
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
|
||||
6
src-tauri/tauri.ios.conf.json
vendored
6
src-tauri/tauri.ios.conf.json
vendored
@@ -11,9 +11,7 @@
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"resources": [
|
||||
"configuration"
|
||||
],
|
||||
"resources": ["configuration"],
|
||||
"targets": "all",
|
||||
"iOS": {
|
||||
"minimumSystemVersion": "13.0"
|
||||
@@ -23,7 +21,7 @@
|
||||
"windows": [
|
||||
{
|
||||
"title": "登录",
|
||||
"label": "login",
|
||||
"label": "mobile-home",
|
||||
"url": "/mobile/login",
|
||||
"resizable": false,
|
||||
"fullscreen": true
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import type { IKeyboardDidShowDetail } from '@/mobile/mobile-client/interface/adapter'
|
||||
import type { IKeyboardDidShowDetail } from '#/mobile-client/interface/adapter'
|
||||
import { useMobileStore } from '@/stores/mobile'
|
||||
import { isMobile } from '@/utils/PlatformConstants'
|
||||
|
||||
export function useMobile() {
|
||||
export const useMobile = () => {
|
||||
const mobileStore = useMobileStore()
|
||||
|
||||
let removeShowFunction: () => void = () => {}
|
||||
@@ -11,7 +11,7 @@ export function useMobile() {
|
||||
|
||||
onMounted(async () => {
|
||||
if (isMobile()) {
|
||||
const module = await import('@/mobile/mobile-client/MobileClient')
|
||||
const module = await import('#/mobile-client/MobileClient')
|
||||
|
||||
await module.initMobileClient()
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'uno.css'
|
||||
import '@unocss/reset/eric-meyer.css' // unocss提供的浏览器默认样式重置
|
||||
import { initMobileClient } from '#/mobile-client/MobileClient'
|
||||
import App from '@/App.vue'
|
||||
import { AppException } from '@/common/exception.ts'
|
||||
import vResize from '@/directives/v-resize'
|
||||
import vSlide from '@/directives/v-slide.ts'
|
||||
import { initMobileClient } from '@/mobile/mobile-client/MobileClient'
|
||||
import router from '@/router'
|
||||
import { pinia } from '@/stores'
|
||||
import { initializePlatform } from '@/utils/PlatformConstants'
|
||||
|
||||
@@ -7,16 +7,21 @@
|
||||
@touchend="handleTouchEnd">
|
||||
<!-- 下拉指示器 -->
|
||||
<div
|
||||
class="refresh-indicator absolute left-0 right-0 z-10 w-full flex-center transform transition-transform duration-300 bg-white/80 backdrop-blur-sm"
|
||||
:class="[isRefreshing ? 'text-primary-500' : 'text-gray-400', { 'opacity-0': distance === 0 }]"
|
||||
class="refresh-indicator absolute left-0 right-0 z-30 w-full flex-center transform bg-transparent backdrop-blur-sm"
|
||||
:class="[
|
||||
isRefreshing ? 'text-primary-500' : 'text-gray-400',
|
||||
{ 'opacity-0': distance === 0 },
|
||||
{ 'transition-transform duration-300': !isDragging }
|
||||
]"
|
||||
:style="{
|
||||
top: '0',
|
||||
height: `${indicatorHeight}px`,
|
||||
transform: `translateY(-${indicatorHeight - distance}px)`
|
||||
transform: `translate3d(0, -${Math.max(indicatorHeight - distance, 0)}px, 0)`,
|
||||
'will-change': isDragging ? 'transform' : ''
|
||||
}">
|
||||
<template v-if="isRefreshing">
|
||||
<n-spin size="small" />
|
||||
<span class="ml-2 text-sm">正在刷新...</span>
|
||||
<img class="size-18px" src="@/assets/img/loading.svg" alt="" />
|
||||
<span class="ml-2 text-sm color-#333">正在刷新...</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex-center flex-col">
|
||||
@@ -38,10 +43,11 @@
|
||||
<!-- 内容区域 -->
|
||||
<div
|
||||
ref="contentRef"
|
||||
class="transform transition-transform duration-300"
|
||||
:class="['transform', { 'transition-transform duration-300': !isDragging }]"
|
||||
:style="{
|
||||
transform: `translateY(${distance}px)`,
|
||||
minHeight: '100%'
|
||||
transform: `translate3d(0, ${distance}px, 0)`,
|
||||
minHeight: '100%',
|
||||
'will-change': isDragging ? 'transform' : ''
|
||||
}">
|
||||
<slot />
|
||||
</div>
|
||||
@@ -70,6 +76,27 @@ const contentRef = ref<HTMLElement>()
|
||||
const distance = ref(0)
|
||||
const startY = ref(0)
|
||||
const isRefreshing = ref(false)
|
||||
const isDragging = ref(false)
|
||||
|
||||
// rAF 降频更新,避免高频触发造成抖动
|
||||
let rafId: number | null = null
|
||||
let pendingDistance: number | null = null
|
||||
|
||||
// 刷新距离
|
||||
const flushDistance = () => {
|
||||
if (pendingDistance === null) return
|
||||
distance.value = pendingDistance
|
||||
pendingDistance = null
|
||||
rafId = null
|
||||
}
|
||||
|
||||
// 节流更新距离
|
||||
const scheduleDistanceUpdate = (val: number) => {
|
||||
pendingDistance = val
|
||||
if (rafId == null) {
|
||||
rafId = requestAnimationFrame(flushDistance)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理触摸开始
|
||||
const handleTouchStart = (e: TouchEvent) => {
|
||||
@@ -79,6 +106,7 @@ const handleTouchStart = (e: TouchEvent) => {
|
||||
// 只有在顶部才能下拉
|
||||
if (scrollTop <= 0) {
|
||||
startY.value = e.touches[0].clientY
|
||||
isDragging.value = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +123,8 @@ const handleTouchMove = (e: TouchEvent) => {
|
||||
if (diff > 0) {
|
||||
e.preventDefault()
|
||||
// 使用阻尼系数让下拉变得越来越困难
|
||||
distance.value = Math.min(diff * 0.4, props.threshold * 1.5)
|
||||
const next = Math.round(Math.min(diff * 0.4, props.threshold * 1.5))
|
||||
scheduleDistanceUpdate(next)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,9 +137,17 @@ const handleTouchEnd = () => {
|
||||
distance.value = props.indicatorHeight
|
||||
emit('refresh')
|
||||
} else {
|
||||
// 回弹到初始位置
|
||||
distance.value = 0
|
||||
}
|
||||
startY.value = 0
|
||||
isDragging.value = false
|
||||
// 取消未完成的 rAF
|
||||
if (rafId != null) {
|
||||
cancelAnimationFrame(rafId)
|
||||
rafId = null
|
||||
pendingDistance = null
|
||||
}
|
||||
}
|
||||
|
||||
// 完成刷新
|
||||
|
||||
@@ -7,14 +7,18 @@
|
||||
<div
|
||||
class="self-center h-auto transition-transform duration-300 ease-in-out origin-top"
|
||||
:style="{ transform: props.isShow ? 'scale(1) translateY(0)' : 'scale(0.62) translateY(0px)' }">
|
||||
<n-avatar :size="86" src="#" fallback-src="/logo.png" round />
|
||||
<n-avatar
|
||||
:size="86"
|
||||
:src="AvatarUtils.getAvatarUrl(userStore.userInfo.avatar!)"
|
||||
fallback-src="/logo.png"
|
||||
round />
|
||||
</div>
|
||||
|
||||
<!-- 基本信息栏 -->
|
||||
<div ref="infoBox" class="pl-2 flex gap-8px flex-col transition-transform duration-300 ease-in-out">
|
||||
<!-- 名字与在线状态 -->
|
||||
<div class="flex flex-warp gap-4 items-center">
|
||||
<span class="font-bold text-20px text-#373838">苏小研</span>
|
||||
<span class="font-bold text-20px text-#373838">{{ userStore.userInfo.name }}</span>
|
||||
<div class="bg-#E7EFE6 flex flex-wrap ps-2 items-center rounded-full gap-1 w-50px h-24px">
|
||||
<span class="w-12px h-12px rounded-15px bg-#079669"></span>
|
||||
<span class="text-bold-style" style="font-size: 12px; color: #373838">在线</span>
|
||||
@@ -22,7 +26,7 @@
|
||||
</div>
|
||||
<!-- 账号 -->
|
||||
<div class="flex flex-warp gap-2 items-center">
|
||||
<span class="text-bold-style">账号:123456789</span>
|
||||
<span class="text-bold-style">账号:{{ userStore.userInfo.account }}</span>
|
||||
<span @click="toMyQRCode" class="pe-15px">
|
||||
<img class="w-14px h-14px" src="@/assets/mobile/my/qr-code.webp" alt="" />
|
||||
</span>
|
||||
@@ -98,6 +102,10 @@
|
||||
<script setup lang="ts">
|
||||
// import router from '@/router'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
|
||||
@@ -6,16 +6,39 @@
|
||||
|
||||
<!-- 页面全部内容 -->
|
||||
<div class="flex flex-col flex-1">
|
||||
<RouterView />
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition name="slide" appear mode="out-in">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</div>
|
||||
|
||||
<!-- 底部安全区域占位元素 -->
|
||||
<SafeAreaPlaceholder type="layout" class="" direction="bottom" />
|
||||
<SafeAreaPlaceholder type="layout" class="bg-#FAFAFA" direction="bottom" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SafeAreaPlaceholder from '@/mobile/components/placeholders/SafeAreaPlaceholder.vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import SafeAreaPlaceholder from '#/components/placeholders/SafeAreaPlaceholder.vue'
|
||||
|
||||
const route = useRoute()
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
/* 侧滑切换动画 */
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
|
||||
.slide-enter-from {
|
||||
transform: translateX(-30px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-leave-to {
|
||||
transform: translateX(30px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,34 @@
|
||||
<template>
|
||||
<!-- 页面全部内容 -->
|
||||
<div class="flex flex-col flex-1">
|
||||
<RouterView />
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition name="slide" appear mode="out-in">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
<style scoped></style>
|
||||
const route = useRoute()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 侧滑切换动画 */
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
|
||||
.slide-enter-from {
|
||||
transform: translateX(-30px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-leave-to {
|
||||
transform: translateX(30px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
44
src/mobile/layout/friends/FriendsLayout.vue
Normal file
44
src/mobile/layout/friends/FriendsLayout.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="h-100vh flex flex-col">
|
||||
<!-- 考虑不需要这个元素,因为有些页面是占满顶部的,考虑按需引入 -->
|
||||
<!-- 顶部安全区域占位元素 -->
|
||||
<SafeAreaPlaceholder type="layout" class="" direction="top" />
|
||||
|
||||
<!-- 页面全部内容 -->
|
||||
<div class="flex flex-col flex-1">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition name="slide" appear mode="out-in">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</div>
|
||||
|
||||
<!-- 底部安全区域占位元素 -->
|
||||
<SafeAreaPlaceholder type="layout" class="" direction="bottom" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router'
|
||||
import SafeAreaPlaceholder from '#/components/placeholders/SafeAreaPlaceholder.vue'
|
||||
|
||||
const route = useRoute()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 侧滑切换动画 */
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
|
||||
.slide-enter-from {
|
||||
transform: translateX(-30px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-leave-to {
|
||||
transform: translateX(30px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="flex-1 overflow-y-auto flex flex-col">
|
||||
<div class="flex flex-1 overflow-y-auto">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition name="slide" appear>
|
||||
<Transition name="slide" appear mode="out-in">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
@@ -24,12 +24,24 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { emitTo } from '@tauri-apps/api/event'
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import { useRoute } from 'vue-router'
|
||||
import SafeAreaPlaceholder from '@/mobile/components/placeholders/SafeAreaPlaceholder.vue'
|
||||
import type { default as TabBarType } from '@/mobile/layout/tabBar/index.vue'
|
||||
import TabBar from '@/mobile/layout/tabBar/index.vue'
|
||||
import SafeAreaPlaceholder from '#/components/placeholders/SafeAreaPlaceholder.vue'
|
||||
import type { default as TabBarType } from '#/layout/tabBar/index.vue'
|
||||
import TabBar from '#/layout/tabBar/index.vue'
|
||||
import { MittEnum, NotificationTypeEnum, TauriCommand } from '@/enums'
|
||||
import { useMitt } from '@/hooks/useMitt'
|
||||
import type { MessageType } from '@/services/types'
|
||||
import { WsResponseMessageType } from '@/services/wsType'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
import { useMobileStore } from '@/stores/mobile'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { audioManager } from '@/utils/AudioManager'
|
||||
import { calculateElementPosition } from '@/utils/DomCalculate'
|
||||
import { invokeSilently } from '@/utils/TauriInvokeHandler'
|
||||
|
||||
const route = useRoute()
|
||||
const mobileStore = useMobileStore()
|
||||
@@ -53,6 +65,78 @@ const updateTabBarPosition = async (isInit: boolean) => {
|
||||
})
|
||||
}
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const userStore = useUserStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const userUid = computed(() => userStore.userInfo.uid)
|
||||
const playMessageSound = async () => {
|
||||
try {
|
||||
const audio = new Audio('/sound/message.mp3')
|
||||
await audioManager.play(audio, 'message-notification')
|
||||
} catch (error) {
|
||||
console.warn('播放消息音效失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/** 测试 */
|
||||
useMitt.on(WsResponseMessageType.RECEIVE_MESSAGE, async (data: MessageType) => {
|
||||
chatStore.pushMsg(data)
|
||||
data.message.sendTime = new Date(data.message.sendTime).getTime()
|
||||
await invokeSilently(TauriCommand.SAVE_MSG, {
|
||||
data
|
||||
})
|
||||
if (data.fromUser.uid !== userUid.value) {
|
||||
// 获取该消息的会话信息
|
||||
const session = chatStore.sessionList.find((s) => s.roomId === data.message.roomId)
|
||||
|
||||
// 只有非免打扰的会话才发送通知和触发图标闪烁
|
||||
if (session && session.muteNotification !== NotificationTypeEnum.NOT_DISTURB) {
|
||||
// 检查 home 窗口状态
|
||||
const home = await WebviewWindow.getByLabel('mobile-home')
|
||||
let shouldPlaySound = false
|
||||
|
||||
if (home) {
|
||||
try {
|
||||
const isVisible = await home.isVisible()
|
||||
const isMinimized = await home.isMinimized()
|
||||
const isFocused = await home.isFocused()
|
||||
|
||||
// 如果窗口不可见、被最小化或未聚焦,则播放音效
|
||||
shouldPlaySound = !isVisible || isMinimized || !isFocused
|
||||
} catch (error) {
|
||||
console.warn('检查窗口状态失败:', error)
|
||||
// 如果检查失败,默认播放音效
|
||||
shouldPlaySound = true
|
||||
}
|
||||
} else {
|
||||
// 如果找不到 home 窗口,播放音效
|
||||
shouldPlaySound = true
|
||||
}
|
||||
|
||||
// 播放消息音效
|
||||
if (shouldPlaySound) {
|
||||
await playMessageSound()
|
||||
}
|
||||
|
||||
// 设置图标闪烁
|
||||
useMitt.emit(MittEnum.MESSAGE_ANIMATION, data)
|
||||
// session.unreadCount++
|
||||
// 在windows系统下才发送通知
|
||||
if (type() === 'windows') {
|
||||
globalStore.setTipVisible(true)
|
||||
}
|
||||
|
||||
if (WebviewWindow.getCurrent().label === 'mobile-home') {
|
||||
await emitTo('notify', 'notify_content', data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await globalStore.updateGlobalUnreadCount()
|
||||
})
|
||||
|
||||
/** 测试-结束 */
|
||||
|
||||
onMounted(async () => {
|
||||
await updateTabBarPosition(true)
|
||||
})
|
||||
@@ -75,16 +159,16 @@ watch(
|
||||
/* 侧滑切换动画 */
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
transition: all 0.15s ease;
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
|
||||
.slide-enter-from {
|
||||
transform: translateX(30px);
|
||||
transform: translateX(-30px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-leave-to {
|
||||
transform: translateX(-30px);
|
||||
transform: translateX(30px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
|
||||
<!-- 页面全部内容 -->
|
||||
<div class="flex flex-col flex-1">
|
||||
<RouterView />
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition name="slide" appear mode="out-in">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</div>
|
||||
|
||||
<!-- 底部安全区域占位元素 -->
|
||||
@@ -15,7 +19,26 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SafeAreaPlaceholder from '@/mobile/components/placeholders/SafeAreaPlaceholder.vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import SafeAreaPlaceholder from '#/components/placeholders/SafeAreaPlaceholder.vue'
|
||||
|
||||
const route = useRoute()
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
/* 侧滑切换动画 */
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
|
||||
.slide-enter-from {
|
||||
transform: translateX(-30px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-leave-to {
|
||||
transform: translateX(30px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div
|
||||
@click="activeTab = 'login'"
|
||||
:class="[
|
||||
'z-999 w-100px text-center transition-all duration-300 ',
|
||||
'z-999 w-100px text-center transition-all duration-300 ease-out',
|
||||
activeTab === 'login' ? 'text-(18px #000)' : 'text-(16px #666)'
|
||||
]">
|
||||
登录
|
||||
@@ -19,7 +19,7 @@
|
||||
<div
|
||||
@click="activeTab = 'register'"
|
||||
:class="[
|
||||
'z-999 w-100px text-center transition-all duration-300 ',
|
||||
'z-999 w-100px text-center transition-all duration-300 ease-out',
|
||||
activeTab === 'register' ? 'text-(18px #000)' : 'text-(16px #666)'
|
||||
]">
|
||||
注册
|
||||
@@ -113,7 +113,7 @@
|
||||
tertiary
|
||||
style="color: #fff"
|
||||
class="w-full mt-8px mb-50px gradient-button"
|
||||
@click="normalLogin">
|
||||
@click="normalLogin(false)">
|
||||
<span>{{ loginText }}</span>
|
||||
</n-button>
|
||||
|
||||
@@ -308,29 +308,30 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { emit } from '@tauri-apps/api/event'
|
||||
import { lightTheme } from 'naive-ui'
|
||||
import { ErrorType } from '@/common/exception'
|
||||
import PinInput from '@/components/common/PinInput.vue'
|
||||
import Validation from '@/components/common/Validation.vue'
|
||||
import { TauriCommand } from '@/enums'
|
||||
import { useLogin } from '@/hooks/useLogin'
|
||||
import type { RegisterUserReq, UserInfoType } from '@/services/types'
|
||||
import { useLoginHistoriesStore } from '@/stores/loginHistory.ts'
|
||||
import { useMobileStore } from '@/stores/mobile'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
import { getCaptcha, getUserDetail, login, register, sendCaptcha } from '@/utils/ImRequestUtils'
|
||||
import { getAllUserState, getCaptcha, getUserDetail, register, sendCaptcha } from '@/utils/ImRequestUtils'
|
||||
import { isAndroid } from '@/utils/PlatformConstants'
|
||||
import { invokeWithErrorHandler } from '@/utils/TauriInvokeHandler'
|
||||
import router from '../router'
|
||||
import rustWebSocketClient from '../services/webSocketRust'
|
||||
import { useUserStatusStore } from '../stores/userStatus'
|
||||
|
||||
// 本地注册信息类型,扩展API类型以包含确认密码
|
||||
interface LocalRegisterInfo extends RegisterUserReq {}
|
||||
|
||||
const loginHistoriesStore = useLoginHistoriesStore()
|
||||
const userStore = useUserStore()
|
||||
const { setLoginState } = useLogin()
|
||||
const { loginHistories } = loginHistoriesStore
|
||||
const mobileStore = useMobileStore()
|
||||
const safeArea = computed(() => mobileStore.safeArea)
|
||||
@@ -359,7 +360,7 @@ const registerInfo = ref<LocalRegisterInfo>({
|
||||
code: '',
|
||||
uuid: '',
|
||||
avatar: '',
|
||||
key: '',
|
||||
key: 'REGISTER_EMAIL',
|
||||
systemType: 2
|
||||
})
|
||||
|
||||
@@ -584,58 +585,128 @@ const handleRegisterComplete = async () => {
|
||||
}
|
||||
|
||||
/**登录后创建主页窗口*/
|
||||
const normalLogin = async () => {
|
||||
const normalLogin = async (auto = false) => {
|
||||
loading.value = true
|
||||
const { account, password } = info.value
|
||||
login({ account, password, deviceType: 'MOBILE', systemType: 2, grantType: 'PASSWORD' })
|
||||
.then(async (res) => {
|
||||
loginText.value = '登录中...'
|
||||
loginDisabled.value = true
|
||||
// 根据auto参数决定从哪里获取登录信息
|
||||
const loginInfo = auto ? (userStore.userInfo as UserInfoType) : info.value
|
||||
const { account } = loginInfo
|
||||
|
||||
// 自动登录
|
||||
if (auto) {
|
||||
// 添加2秒延迟
|
||||
await new Promise((resolve) => setTimeout(resolve, 1200))
|
||||
|
||||
// TODO 自动登录
|
||||
// try {
|
||||
// // 登录处理
|
||||
//
|
||||
// loginProcess(null, null, null)
|
||||
// } catch (error) {
|
||||
// console.error('自动登录失败', error)
|
||||
// // 如果是网络异常,不删除token
|
||||
// if (!isOnline.value) {
|
||||
// loginDisabled.value = true
|
||||
// loginText.value = '网络异常'
|
||||
// loading.value = false
|
||||
// } else {
|
||||
// // 其他错误才清除token并重置状态
|
||||
// localStorage.removeItem('TOKEN')
|
||||
// isAutoLogin.value = false
|
||||
// loginDisabled.value = true
|
||||
// loginText.value = '登录'
|
||||
// loading.value = false
|
||||
// }
|
||||
// }
|
||||
return
|
||||
}
|
||||
|
||||
invoke('login_command', {
|
||||
data: {
|
||||
account: account,
|
||||
password: info.value.password,
|
||||
deviceType: 'MOBILE',
|
||||
systemType: '2', // 2是im 1是后台
|
||||
grantType: 'PASSWORD'
|
||||
}
|
||||
})
|
||||
.then(async (res: any) => {
|
||||
loginDisabled.value = true
|
||||
loginText.value = '登录成功, 正在跳转'
|
||||
userStore.isSign = true
|
||||
// 存储双token
|
||||
localStorage.setItem('TOKEN', res.token)
|
||||
localStorage.setItem('REFRESH_TOKEN', res.refreshToken)
|
||||
// 需要删除二维码,因为用户可能先跳转到二维码界面再回到登录界面,会导致二维码一直保持在内存中
|
||||
if (localStorage.getItem('wsLogin')) {
|
||||
localStorage.removeItem('wsLogin')
|
||||
}
|
||||
// 获取用户详情
|
||||
const userDetail: any = await getUserDetail()
|
||||
// TODO 先不获取 emoji 列表,当我点击 emoji 按钮的时候再获取
|
||||
// await emojiStore.getEmojiList()
|
||||
// TODO 这里的id暂时赋值给uid,因为后端没有统一返回uid,待后端调整
|
||||
const account = {
|
||||
...userDetail,
|
||||
token: res.token,
|
||||
client: res.client
|
||||
}
|
||||
await invokeWithErrorHandler(
|
||||
TauriCommand.SAVE_USER_INFO,
|
||||
{
|
||||
userInfo: account
|
||||
},
|
||||
{
|
||||
customErrorMessage: '保存用户信息失败',
|
||||
errorType: ErrorType.Client
|
||||
}
|
||||
)
|
||||
console.log('登录成功')
|
||||
|
||||
await emit('set_user_info', {
|
||||
token: res.token,
|
||||
refreshToken: res.refreshToken,
|
||||
uid: account.uid
|
||||
})
|
||||
// 开启 ws 连接
|
||||
await rustWebSocketClient.initConnect()
|
||||
// 登录处理
|
||||
await loginProcess(res.token, res.refreshToken, res.client)
|
||||
})
|
||||
.catch((e: any) => {
|
||||
console.error('登录异常:', e)
|
||||
loading.value = false
|
||||
loginDisabled.value = false
|
||||
loginText.value = '登录'
|
||||
// 如果是自动登录失败,重置按钮状态允许手动登录
|
||||
if (auto) {
|
||||
loginDisabled.value = false
|
||||
loginText.value = '登录'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
userStore.userInfo = account
|
||||
loginHistoriesStore.addLoginHistory(account)
|
||||
router.push('/mobile/message')
|
||||
await setLoginState()
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false
|
||||
console.error(e)
|
||||
})
|
||||
const userStatusStore = useUserStatusStore()
|
||||
const { stateId } = storeToRefs(userStatusStore)
|
||||
|
||||
const loginProcess = async (token: string, refreshToken: string, client: string) => {
|
||||
loading.value = false
|
||||
// 获取用户状态列表
|
||||
if (userStatusStore.stateList.length === 0) {
|
||||
try {
|
||||
userStatusStore.stateList = await getAllUserState()
|
||||
} catch (error) {
|
||||
console.error('获取用户状态列表失败', error)
|
||||
}
|
||||
}
|
||||
// 获取用户详情
|
||||
// const userDetail = await apis.getUserDetail()
|
||||
const userDetail: any = await getUserDetail()
|
||||
|
||||
// 设置用户状态id
|
||||
stateId.value = userDetail.userStateId
|
||||
// const token = localStorage.getItem('TOKEN')
|
||||
// const refreshToken = localStorage.getItem('REFRESH_TOKEN')
|
||||
// TODO 先不获取 emoji 列表,当我点击 emoji 按钮的时候再获取
|
||||
// await emojiStore.getEmojiList()
|
||||
const account = {
|
||||
...userDetail,
|
||||
token,
|
||||
refreshToken,
|
||||
client
|
||||
}
|
||||
userStore.userInfo = account
|
||||
loginHistoriesStore.addLoginHistory(account)
|
||||
// 在 sqlite 中存储用户信息
|
||||
await invokeWithErrorHandler(
|
||||
TauriCommand.SAVE_USER_INFO,
|
||||
{
|
||||
userInfo: userDetail
|
||||
},
|
||||
{
|
||||
customErrorMessage: '保存用户信息失败',
|
||||
errorType: ErrorType.Client
|
||||
}
|
||||
)
|
||||
|
||||
// 在 rust 部分设置 token
|
||||
await emit('set_user_info', {
|
||||
token,
|
||||
refreshToken,
|
||||
uid: userDetail.uid
|
||||
})
|
||||
|
||||
loginText.value = '登录成功正在跳转...'
|
||||
|
||||
router.push('/mobile/message')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,6 @@ import type {
|
||||
} from './interface/adapter'
|
||||
|
||||
export class IosAdapter implements IMobileClientAdapter {
|
||||
// TODO 着个没测试过,需要测试才行
|
||||
keyboardListener(showCallback: TKeyboardShowCallback, hideCallback: TKeyboardHideCallback): KeyboardListenerResult {
|
||||
const showHandler = (e: Event) => {
|
||||
const detail = (e as CustomEvent<IKeyboardDidShowDetail>).detail
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<AutoFixHeightPage>
|
||||
<template #header>
|
||||
<HeaderBar ref="header" :room-name="roomName" :msg-count="1002" />
|
||||
<HeaderBar ref="header" :room-name="currentChatRoom.name" :msg-count="1002" />
|
||||
</template>
|
||||
<template #container>
|
||||
<!-- 网络状态提示 -->
|
||||
@@ -442,13 +442,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AutoFixHeightPage from '@/mobile/components/chat-room/AutoFixHeightPage.vue'
|
||||
import FooterBar from '@/mobile/components/chat-room/FooterBar.vue'
|
||||
import HeaderBar from '@/mobile/components/chat-room/HeaderBar.vue'
|
||||
import AutoFixHeightPage from '#/components/chat-room/AutoFixHeightPage.vue'
|
||||
import FooterBar from '#/components/chat-room/FooterBar.vue'
|
||||
import HeaderBar from '#/components/chat-room/HeaderBar.vue'
|
||||
import { useMobileStore } from '@/stores/mobile'
|
||||
import { markMsg } from '@/utils/ImRequestUtils'
|
||||
|
||||
const route = useRoute()
|
||||
const roomName = ref(route.params.roomName as string)
|
||||
const mobileStore = useMobileStore()
|
||||
|
||||
const currentChatRoom = mobileStore.currentChatRoom
|
||||
|
||||
const header = ref()
|
||||
|
||||
|
||||
@@ -41,9 +41,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CommunityContent from '@/mobile/components/community/CommunityContent.vue'
|
||||
import CommunityTab from '@/mobile/components/community/CommunityTab.vue'
|
||||
import SafeAreaPlaceholder from '@/mobile/components/placeholders/SafeAreaPlaceholder.vue'
|
||||
import CommunityContent from '#/components/community/CommunityContent.vue'
|
||||
import CommunityTab from '#/components/community/CommunityTab.vue'
|
||||
import SafeAreaPlaceholder from '#/components/placeholders/SafeAreaPlaceholder.vue'
|
||||
import router from '@/router'
|
||||
|
||||
const toScanQRCode = () => [router.push('/mobile/mobileMy/scanQRcode')]
|
||||
|
||||
277
src/mobile/views/friends/AddFriends.vue
Normal file
277
src/mobile/views/friends/AddFriends.vue
Normal file
@@ -0,0 +1,277 @@
|
||||
<template>
|
||||
<div class="flex w-full flex-col gap-3">
|
||||
<!-- 键盘蒙板 -->
|
||||
<div
|
||||
v-if="showKeyboardMask"
|
||||
class="keyboard-mask flex-1"
|
||||
@touchstart.stop.prevent="closeKeyboardMask"
|
||||
@click.stop.prevent="closeKeyboardMask"></div>
|
||||
|
||||
<div class="px-16px mt-5px flex gap-3">
|
||||
<div class="flex-1 py-5px shrink-0">
|
||||
<n-input
|
||||
id="search"
|
||||
class="rounded-10px w-full bg-gray-100 relative text-14px"
|
||||
:maxlength="20"
|
||||
clearable
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
placeholder="输入用户名字/账号搜索~"
|
||||
@focus="lockScroll"
|
||||
@blur="unlockScroll">
|
||||
<template #prefix>
|
||||
<svg class="w-12px h-12px"><use href="#search"></use></svg>
|
||||
</template>
|
||||
</n-input>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end items-center">
|
||||
<n-button class="py-5px">搜索</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="scrollArea"
|
||||
id="scrollArea"
|
||||
:style="{ height: scrollHeight + 'px' }"
|
||||
class="px-16px overflow-y-auto scroll-auto h-100px">
|
||||
<div class="flex flex-1 flex-col gap-2">
|
||||
<n-tabs type="segment" animated class="mt-4px">
|
||||
<!-- 用户 -->
|
||||
<n-tab-pane name="1" tab="用户">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="font-bold"></span>
|
||||
<span class="text-(10px #707070)">{{ onlineCount }}/{{ contactStore.contactsList.length }}</span>
|
||||
</div>
|
||||
<n-scrollbar style="max-height: calc(100vh - 220px)">
|
||||
<div @contextmenu.stop="$event.preventDefault()">
|
||||
<n-flex
|
||||
:size="10"
|
||||
@click="handleClick(item.uid, RoomTypeEnum.SINGLE)"
|
||||
:class="{ active: activeItem === item.uid }"
|
||||
class="item-box w-full h-75px mb-5px"
|
||||
v-for="item in sortedContacts"
|
||||
:key="item.uid">
|
||||
<n-flex
|
||||
align="center"
|
||||
justify="space-between"
|
||||
:size="10"
|
||||
class="h-75px pl-6px pr-8px flex-1 truncate">
|
||||
<!-- 左边用户信息 -->
|
||||
<n-flex align="center" :size="10" class="flex-1 truncate">
|
||||
<n-avatar
|
||||
round
|
||||
style="border: 1px solid var(--avatar-border-color)"
|
||||
:size="44"
|
||||
class="grayscale"
|
||||
:class="{ 'grayscale-0': item.activeStatus === OnlineEnum.ONLINE }"
|
||||
:src="AvatarUtils.getAvatarUrl(useUserInfo(item.uid).value.avatar!)"
|
||||
fallback-src="/logo.png" />
|
||||
|
||||
<n-flex vertical justify="space-between" class="h-fit flex-1 truncate">
|
||||
<span class="text-14px leading-tight flex-1 truncate">
|
||||
{{ useUserInfo(item.uid).value.name }}
|
||||
</span>
|
||||
<div class="text leading-tight text-12px flex-y-center gap-4px flex-1 truncate">
|
||||
[
|
||||
<template v-if="getUserState(item.uid)">
|
||||
<img class="size-12px rounded-50%" :src="getUserState(item.uid)?.url" alt="" />
|
||||
{{ getUserState(item.uid)?.title }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<n-badge :color="item.activeStatus === OnlineEnum.ONLINE ? '#1ab292' : '#909090'" dot />
|
||||
{{ item.activeStatus === OnlineEnum.ONLINE ? '在线' : '离线' }}
|
||||
</template>
|
||||
]
|
||||
</div>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<!-- 右边操作按钮 -->
|
||||
<n-button size="small" @click.stop="addFriend(item.uid)">添加</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 群聊 -->
|
||||
<n-tab-pane name="2" tab="群聊">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="font-bold"></span>
|
||||
<span class="text-(10px #707070)">{{ groupChatList.length }}</span>
|
||||
</div>
|
||||
<n-scrollbar style="max-height: calc(100vh - 220px)">
|
||||
<div
|
||||
@click="handleClick(item.roomId, RoomTypeEnum.GROUP)"
|
||||
:class="{ active: activeItem === item.roomId }"
|
||||
class="item-box w-full h-75px mb-5px"
|
||||
v-for="item in groupChatList"
|
||||
:key="item.roomId">
|
||||
<n-flex align="center" justify="space-between" :size="10" class="h-75px pl-6px pr-8px flex-1 truncate">
|
||||
<!-- 左边群聊信息 -->
|
||||
<n-flex align="center" :size="10" class="flex-1 truncate">
|
||||
<n-avatar
|
||||
round
|
||||
style="border: 1px solid var(--avatar-border-color)"
|
||||
bordered
|
||||
:size="44"
|
||||
:src="AvatarUtils.getAvatarUrl(item.avatar)"
|
||||
fallback-src="/logo.png" />
|
||||
<span class="text-14px leading-tight flex-1 truncate">{{ item.remark || item.roomName }}</span>
|
||||
</n-flex>
|
||||
|
||||
<!-- 右边操作按钮 -->
|
||||
<n-button size="small" @click.stop="joinGroup(item.roomId)">添加</n-button>
|
||||
</n-flex>
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { MittEnum, OnlineEnum, RoomTypeEnum } from '@/enums'
|
||||
import { useUserInfo } from '@/hooks/useCached.ts'
|
||||
import { useMitt } from '@/hooks/useMitt.ts'
|
||||
import { useChatStore } from '@/stores/chat.ts'
|
||||
import { useContactStore } from '@/stores/contacts.ts'
|
||||
import { useUserStatusStore } from '@/stores/userStatus'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
import { useMobileStore } from '~/src/stores/mobile'
|
||||
import { calculateElementPosition } from '~/src/utils/DomCalculate'
|
||||
|
||||
const scrollArea = ref<HTMLDivElement>()
|
||||
|
||||
const mobileStore = useMobileStore()
|
||||
|
||||
const scrollHeight = ref(700) // 默认高度
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const scrollAreaRect = await calculateElementPosition(scrollArea)
|
||||
scrollHeight.value = window.innerHeight - scrollAreaRect!.y - mobileStore.safeArea.bottom
|
||||
} catch (error) {
|
||||
console.log('计算[scrollArea]高度失败:', error)
|
||||
}
|
||||
})
|
||||
|
||||
// 锁滚动(和蒙板一样)
|
||||
const lockScroll = () => {
|
||||
const scrollEl = document.querySelector('#scrollArea') as HTMLElement
|
||||
if (scrollEl) {
|
||||
scrollEl.style.overflow = 'hidden'
|
||||
}
|
||||
}
|
||||
|
||||
const unlockScroll = () => {
|
||||
const scrollEl = document.querySelector('#scrollArea') as HTMLElement
|
||||
if (scrollEl) {
|
||||
scrollEl.style.overflow = 'auto'
|
||||
}
|
||||
}
|
||||
|
||||
// 键盘蒙板显示状态
|
||||
const showKeyboardMask = ref(false)
|
||||
|
||||
const closeKeyboardMask = () => {
|
||||
showKeyboardMask.value = false
|
||||
document.body.style.overflow = ''
|
||||
document.body.style.position = ''
|
||||
// 让 input 失焦
|
||||
const activeEl = document.activeElement as HTMLElement
|
||||
if (activeEl && typeof activeEl.blur === 'function') {
|
||||
activeEl.blur()
|
||||
}
|
||||
}
|
||||
|
||||
const addFriend = (_item: any) => {}
|
||||
|
||||
const joinGroup = (_item: any) => {}
|
||||
|
||||
/** 建议把此状态存入localStorage中 */
|
||||
const activeItem = ref('')
|
||||
const detailsShow = ref(false)
|
||||
const shrinkStatus = ref(false)
|
||||
const contactStore = useContactStore()
|
||||
const userStatusStore = useUserStatusStore()
|
||||
const { stateList } = storeToRefs(userStatusStore)
|
||||
|
||||
/** 群聊列表 */
|
||||
const groupChatList = computed(() => {
|
||||
console.log(contactStore.groupChatList)
|
||||
return [...contactStore.groupChatList].sort((a, b) => {
|
||||
// 将roomId为'1'的群聊排在最前面
|
||||
if (a.roomId === '1' && b.roomId !== '1') return -1
|
||||
if (a.roomId !== '1' && b.roomId === '1') return 1
|
||||
return 0
|
||||
})
|
||||
})
|
||||
/** 统计在线用户人数 */
|
||||
const onlineCount = computed(() => {
|
||||
return contactStore.contactsList.filter((item) => item.activeStatus === OnlineEnum.ONLINE).length
|
||||
})
|
||||
/** 排序好友列表 */
|
||||
const sortedContacts = computed(() => {
|
||||
return [...contactStore.contactsList].sort((a, b) => {
|
||||
// 在线用户排在前面
|
||||
if (a.activeStatus === OnlineEnum.ONLINE && b.activeStatus !== OnlineEnum.ONLINE) return -1
|
||||
if (a.activeStatus !== OnlineEnum.ONLINE && b.activeStatus === OnlineEnum.ONLINE) return 1
|
||||
return 0
|
||||
})
|
||||
})
|
||||
/** 监听独立窗口关闭事件 */
|
||||
watchEffect(() => {
|
||||
useMitt.on(MittEnum.SHRINK_WINDOW, async (event) => {
|
||||
shrinkStatus.value = event as boolean
|
||||
})
|
||||
})
|
||||
|
||||
const handleClick = (index: string, type: number) => {
|
||||
detailsShow.value = true
|
||||
activeItem.value = index
|
||||
const data = {
|
||||
context: {
|
||||
type: type,
|
||||
uid: index
|
||||
},
|
||||
detailsShow: detailsShow.value
|
||||
}
|
||||
useMitt.emit(MittEnum.DETAILS_SHOW, data)
|
||||
}
|
||||
|
||||
/** 获取用户状态 */
|
||||
const getUserState = (uid: string) => {
|
||||
const userInfo = useUserInfo(uid).value
|
||||
const userStateId = userInfo.userStateId
|
||||
|
||||
if (userStateId && userStateId !== '1') {
|
||||
return stateList.value.find((state: { id: string }) => state.id === userStateId)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
detailsShow.value = false
|
||||
useMitt.emit(MittEnum.DETAILS_SHOW, detailsShow.value)
|
||||
})
|
||||
|
||||
const chatStore = useChatStore()
|
||||
|
||||
const getSessionList = async () => {
|
||||
await chatStore.getSessionList(true)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getSessionList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
146
src/mobile/views/friends/StartGroupChat.vue
Normal file
146
src/mobile/views/friends/StartGroupChat.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="flex w-full flex-col h-full">
|
||||
<!-- 顶部搜索框 -->
|
||||
<div class="px-16px mt-10px flex gap-3">
|
||||
<div class="flex-1 py-5px shrink-0">
|
||||
<n-input
|
||||
v-model:value="keyword"
|
||||
class="rounded-10px w-full bg-gray-100 relative text-14px"
|
||||
placeholder="搜索联系人~"
|
||||
clearable
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off">
|
||||
<template #prefix>
|
||||
<svg class="w-12px h-12px"><use href="#search"></use></svg>
|
||||
</template>
|
||||
</n-input>
|
||||
</div>
|
||||
<div class="flex justify-end items-center">
|
||||
<n-button class="py-5px" @click="doSearch">搜索</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 联系人列表 -->
|
||||
<!-- 联系人列表 -->
|
||||
<div ref="scrollArea" class="flex-1 overflow-y-auto px-16px mt-10px" :style="{ height: scrollHeight + 'px' }">
|
||||
<n-scrollbar style="max-height: calc(100vh - 150px)">
|
||||
<n-checkbox-group v-model:value="selectedList" class="flex flex-col gap-2">
|
||||
<div
|
||||
v-for="item in filteredContacts"
|
||||
:key="item.uid"
|
||||
class="rounded-10px border border-gray-200 overflow-hidden">
|
||||
<n-checkbox
|
||||
:value="item.uid"
|
||||
size="large"
|
||||
class="w-full flex items-center px-5px"
|
||||
:class="[
|
||||
'cursor-pointer select-none transition-colors duration-150',
|
||||
selectedList.includes(item.uid) ? 'bg-blue-50 border-blue-300' : 'hover:bg-gray-50'
|
||||
]">
|
||||
<template #default>
|
||||
<!-- ✅ 强制一行展示 -->
|
||||
<div class="flex items-center gap-10px px-8px py-10px">
|
||||
<!-- 头像 -->
|
||||
<n-avatar
|
||||
round
|
||||
:size="44"
|
||||
:src="AvatarUtils.getAvatarUrl(useUserInfo(item.uid).value.avatar!)"
|
||||
fallback-src="/logo.png"
|
||||
style="border: 1px solid var(--avatar-border-color)" />
|
||||
<!-- 文字信息 -->
|
||||
<div class="flex flex-col leading-tight truncate">
|
||||
<span class="text-14px font-medium truncate">
|
||||
{{ useUserInfo(item.uid).value.name }}
|
||||
</span>
|
||||
<div class="text-12px text-gray-500 flex items-center gap-4px truncate">
|
||||
<template v-if="getUserState(item.uid)">
|
||||
<img class="size-12px rounded-50%" :src="getUserState(item.uid)?.url" alt="" />
|
||||
{{ getUserState(item.uid)?.title }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<n-badge :color="item.activeStatus === OnlineEnum.ONLINE ? '#1ab292' : '#909090'" dot />
|
||||
{{ item.activeStatus === OnlineEnum.ONLINE ? '在线' : '离线' }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</n-checkbox>
|
||||
</div>
|
||||
</n-checkbox-group>
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<div class="px-16px py-10px bg-white border-t border-gray-200 flex justify-between items-center">
|
||||
<span class="text-14px">已选择 {{ selectedList.length }} 人</span>
|
||||
<n-button type="primary" :disabled="selectedList.length === 0" @click="createGroup">发起群聊</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { OnlineEnum } from '@/enums'
|
||||
import { useUserInfo } from '@/hooks/useCached.ts'
|
||||
import { useContactStore } from '@/stores/contacts'
|
||||
import { useUserStatusStore } from '@/stores/userStatus'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
|
||||
const userStatusStore = useUserStatusStore()
|
||||
const { stateList } = storeToRefs(userStatusStore)
|
||||
|
||||
/** 获取用户状态 */
|
||||
const getUserState = (uid: string) => {
|
||||
const userInfo = useUserInfo(uid).value
|
||||
const userStateId = userInfo.userStateId
|
||||
|
||||
if (userStateId && userStateId !== '1') {
|
||||
return stateList.value.find((state: { id: string }) => state.id === userStateId)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// store
|
||||
const contactStore = useContactStore()
|
||||
|
||||
// 搜索关键字
|
||||
const keyword = ref('')
|
||||
|
||||
// 选中的联系人 uid 数组
|
||||
const selectedList = ref<string[]>([])
|
||||
|
||||
// 滚动高度计算
|
||||
const scrollHeight = ref(600)
|
||||
onMounted(() => {
|
||||
scrollHeight.value = window.innerHeight - 180
|
||||
})
|
||||
|
||||
// 搜索逻辑
|
||||
const doSearch = () => {
|
||||
// 这里只是触发响应式,实际过滤逻辑写在 computed 里
|
||||
}
|
||||
|
||||
const filteredContacts = computed(() => {
|
||||
if (!keyword.value) return contactStore.contactsList
|
||||
return contactStore.contactsList.filter((c) => {
|
||||
const name = useUserInfo(c.uid).value.name
|
||||
if (name) {
|
||||
name.includes(keyword.value)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 点击发起群聊
|
||||
const createGroup = () => {
|
||||
console.log('发起群聊,选择的用户:', selectedList.value)
|
||||
// TODO: 调用接口 / store 创建群聊
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -174,14 +174,14 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import SafeAreaPlaceholder from '#/components/placeholders/SafeAreaPlaceholder.vue'
|
||||
import NavBar from '#/layout/navBar/index.vue'
|
||||
import addFriendIcon from '@/assets/mobile/chat-home/add-friend.webp'
|
||||
import groupChatIcon from '@/assets/mobile/chat-home/group-chat.webp'
|
||||
import { MittEnum, OnlineEnum, RoomTypeEnum } from '@/enums'
|
||||
import { useUserInfo } from '@/hooks/useCached.ts'
|
||||
|
||||
import { useMitt } from '@/hooks/useMitt.ts'
|
||||
import SafeAreaPlaceholder from '@/mobile/components/placeholders/SafeAreaPlaceholder.vue'
|
||||
import NavBar from '@/mobile/layout/navBar/index.vue'
|
||||
import router from '@/router'
|
||||
import { useChatStore } from '@/stores/chat.ts'
|
||||
import { useContactStore } from '@/stores/contacts.ts'
|
||||
import { useUserStatusStore } from '@/stores/userStatus'
|
||||
@@ -208,12 +208,12 @@ const uiViewsData = ref({
|
||||
addOptions: [
|
||||
{
|
||||
label: '发起群聊',
|
||||
key: 'profile',
|
||||
key: '/mobile/mobileFriends/startGroupChat',
|
||||
icon: renderImgIcon(groupChatIcon)
|
||||
},
|
||||
{
|
||||
label: '加好友/群',
|
||||
key: 'editProfile',
|
||||
key: '/mobile/mobileFriends/addFriends',
|
||||
icon: renderImgIcon(addFriendIcon)
|
||||
}
|
||||
]
|
||||
@@ -359,7 +359,9 @@ const addIconHandler = {
|
||||
/**
|
||||
* 选项选择时关闭蒙板
|
||||
*/
|
||||
select: () => {
|
||||
select: (item: string) => {
|
||||
console.log('选择的项:', item)
|
||||
router.push(item)
|
||||
maskHandler.close()
|
||||
},
|
||||
|
||||
|
||||
@@ -12,9 +12,16 @@
|
||||
@click="maskHandler.close"
|
||||
class="fixed inset-0 bg-black/20 backdrop-blur-sm z-[999] transition-all duration-3000 ease-in-out opacity-100"></div>
|
||||
|
||||
<!-- 键盘蒙板 -->
|
||||
<div
|
||||
v-if="showKeyboardMask"
|
||||
class="keyboard-mask flex-1"
|
||||
@touchstart.stop.prevent="closeKeyboardMask"
|
||||
@click.stop.prevent="closeKeyboardMask"></div>
|
||||
|
||||
<NavBar>
|
||||
<template #left>
|
||||
<n-flex align="center" :size="6" class="w-full">
|
||||
<n-flex @click="toSimpleBio" align="center" :size="6" class="w-full">
|
||||
<n-avatar
|
||||
:size="38"
|
||||
:src="AvatarUtils.getAvatarUrl(userStore.userInfo.avatar!)"
|
||||
@@ -33,7 +40,7 @@
|
||||
class="text-(16px [--text-color])">
|
||||
{{ userStore.userInfo.name }}
|
||||
</p>
|
||||
<p class="text-(10px [--text-color])">☁️ 柳州鱼峰</p>
|
||||
<p class="text-(10px [--text-color])">{{ useUserInfo(userStore.userInfo.uid).value.locPlace || '未知' }}</p>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</template>
|
||||
@@ -52,54 +59,54 @@
|
||||
</template>
|
||||
</NavBar>
|
||||
|
||||
<div class="px-16px mt-5px">
|
||||
<div class="py-5px shrink-0">
|
||||
<n-input
|
||||
id="search"
|
||||
class="rounded-6px w-full bg-white relative text-12px"
|
||||
:maxlength="20"
|
||||
clearable
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
placeholder="搜索"
|
||||
@focus="lockScroll"
|
||||
@blur="unlockScroll">
|
||||
<template #prefix>
|
||||
<svg class="w-12px h-12px"><use href="#search"></use></svg>
|
||||
</template>
|
||||
</n-input>
|
||||
</div>
|
||||
<div class="border-b-1 border-solid color-gray-200 px-18px mt-5px"></div>
|
||||
</div>
|
||||
|
||||
<PullToRefresh class="flex-1 overflow-auto" @refresh="handleRefresh" ref="pullRefreshRef">
|
||||
<div class="flex flex-col h-full px-18px">
|
||||
<div class="py-8px shrink-0">
|
||||
<n-input
|
||||
id="search"
|
||||
class="rounded-6px w-full bg-white 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>
|
||||
</template>
|
||||
</n-input>
|
||||
</div>
|
||||
|
||||
<div class="border-b-1 border-solid color-gray-200 px-18px my-8px"></div>
|
||||
|
||||
<div class="flex-1">
|
||||
<div
|
||||
v-for="item in messageItems"
|
||||
:key="item.id"
|
||||
v-for="(item, idx) in messageItems"
|
||||
:key="`${item.id}-${idx}`"
|
||||
@click="intoRoom(item)"
|
||||
class="grid grid-cols-[2.2rem_1fr_4rem] items-start px-2 py-3 gap-1 active:bg-#DEEDE7 active:rounded-10px transition-colors cursor-pointer">
|
||||
class="grid grid-cols-[2.2rem_1fr_4rem] items-start px-2 py-3 gap-1">
|
||||
<!-- 头像:单独居中 -->
|
||||
<div class="self-center h-38px">
|
||||
<n-badge :value="item.unreadCount">
|
||||
<n-avatar :size="40" :src="AvatarUtils.getAvatarUrl(item.avatar)" fallback-src="/logo.png" round />
|
||||
<div class="flex-shrink-0">
|
||||
<n-badge :offset="[-6, 6]" :value="item.unreadCount" :max="99">
|
||||
<n-avatar :size="52" :src="AvatarUtils.getAvatarUrl(item.avatar)" fallback-src="/logo.png" round />
|
||||
</n-badge>
|
||||
</div>
|
||||
|
||||
<!-- {{ item }} -->
|
||||
|
||||
<!-- 中间:两行内容 -->
|
||||
<div class="truncate pl-4 flex gap-10px flex-col">
|
||||
<div class="text-14px leading-tight font-bold flex-1 truncate text-#333 truncate">{{ item.name }}</div>
|
||||
<div class="truncate pl-7 flex pt-5px gap-10px flex-col">
|
||||
<div class="text-16px leading-tight font-bold flex-1 truncate text-#333 truncate">{{ item.name }}</div>
|
||||
<div class="text-12px text-#333 truncate">
|
||||
{{ item.text }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 时间:靠顶 -->
|
||||
<div class="text-12px text-right flex gap-1 items-center justify-right">
|
||||
<div class="text-12px pt-9px text-right flex gap-1 items-center justify-right">
|
||||
<span v-if="item.hotFlag === IsAllUserEnum.Yes">
|
||||
<svg class="size-20px select-none outline-none cursor-pointer color-#13987f">
|
||||
<svg class="size-22px select-none outline-none cursor-pointer color-#13987f">
|
||||
<use href="#auth"></use>
|
||||
</svg>
|
||||
</span>
|
||||
@@ -108,25 +115,6 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div v-for="item in messageItems" :key="item.id" class="message-item relative">
|
||||
<div>
|
||||
<n-badge :value="item.unreadCount">
|
||||
<n-avatar :size="40" :src="item.avatar" fallback-src="/logo.png" round />
|
||||
</n-badge>
|
||||
<div class="flex flex-col ml-14px justify-between h-[35px]">
|
||||
<span class="text-14px text-#333">{{ item.name }}</span>
|
||||
<span class="text-12px text-#999">{{ item.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-right text text-12px w-fit truncate text-right">
|
||||
{{ formatTimestamp(item?.activeTime) }}
|
||||
</div>
|
||||
未读数 悬浮到头像上
|
||||
<div v-if="item.unreadCount > 0" class="flex flex-col justify-between h-[35px] absolute left-30px top-5px">
|
||||
<span class="text-12px text-[#fff] bg-[red] rounded-full px-8px py-3px">{{ item.unreadCount }}</span>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</PullToRefresh>
|
||||
@@ -134,18 +122,24 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import PullToRefresh from '#/components/PullToRefresh.vue'
|
||||
import SafeAreaPlaceholder from '#/components/placeholders/SafeAreaPlaceholder.vue'
|
||||
import NavBar from '#/layout/navBar/index.vue'
|
||||
import type { IKeyboardDidShowDetail } from '#/mobile-client/interface/adapter'
|
||||
import { mobileClient } from '#/mobile-client/MobileClient'
|
||||
import addFriendIcon from '@/assets/mobile/chat-home/add-friend.webp'
|
||||
import groupChatIcon from '@/assets/mobile/chat-home/group-chat.webp'
|
||||
import { useUserInfo } from '@/hooks/useCached.ts'
|
||||
import { useMessage } from '@/hooks/useMessage.ts'
|
||||
import SafeAreaPlaceholder from '@/mobile/components/placeholders/SafeAreaPlaceholder.vue'
|
||||
import NavBar from '@/mobile/layout/navBar/index.vue'
|
||||
import { IsAllUserEnum } from '@/services/types.ts'
|
||||
import rustWebSocketClient from '@/services/webSocketRust'
|
||||
import { useChatStore } from '@/stores/chat.ts'
|
||||
import { useUserStore } from '@/stores/user.ts'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
import { formatTimestamp } from '@/utils/ComputedTime.ts'
|
||||
import { MittEnum } from '~/src/enums'
|
||||
import { useMitt } from '~/src/hooks/useMitt'
|
||||
import { useMobileStore } from '~/src/stores/mobile'
|
||||
|
||||
const chatStore = useChatStore()
|
||||
|
||||
@@ -157,8 +151,43 @@ const getSessionList = async () => {
|
||||
await chatStore.getSessionList(true)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
/**
|
||||
* export interface IKeyboardDidShowDetail {
|
||||
bottomInset: number
|
||||
height: number
|
||||
keyboardVisible: boolean
|
||||
screenHeight: number
|
||||
timestamp: number
|
||||
visibleHeight: number
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
onMounted(async () => {
|
||||
await rustWebSocketClient.setupBusinessMessageListeners()
|
||||
console.log('个人数据:', userStore.userInfo)
|
||||
getSessionList()
|
||||
|
||||
const { removeHideFunction, removeShowFunction } = await mobileClient.keyboardListener(
|
||||
// 键盘打开
|
||||
(detail: IKeyboardDidShowDetail) => {
|
||||
console.log('键盘打开', detail)
|
||||
openKeyboardMask()
|
||||
},
|
||||
// 键盘关闭
|
||||
() => {
|
||||
console.log('键盘关闭')
|
||||
closeKeyboardMask()
|
||||
}
|
||||
)
|
||||
|
||||
// 如果需要在组件卸载时移除监听
|
||||
onBeforeUnmount(() => {
|
||||
removeHideFunction()
|
||||
removeShowFunction()
|
||||
})
|
||||
})
|
||||
|
||||
const handleRefresh = async () => {
|
||||
@@ -167,8 +196,6 @@ const handleRefresh = async () => {
|
||||
pullRefreshRef.value?.finishRefresh()
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
/**
|
||||
* 渲染图片图标的函数工厂
|
||||
* @param {string} src - 图标图片路径
|
||||
@@ -178,7 +205,7 @@ const renderImgIcon = (src: string) => {
|
||||
return () =>
|
||||
h('img', {
|
||||
src,
|
||||
style: 'display:block; width: 24px; height: 24px; vertical-align: middle'
|
||||
style: 'display:block; width: 26px; height: 26px; vertical-align: middle;'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -190,12 +217,12 @@ const uiViewsData = ref({
|
||||
addOptions: [
|
||||
{
|
||||
label: '发起群聊',
|
||||
key: 'profile',
|
||||
key: '/mobile/mobileFriends/startGroupChat',
|
||||
icon: renderImgIcon(groupChatIcon)
|
||||
},
|
||||
{
|
||||
label: '加好友/群',
|
||||
key: 'editProfile',
|
||||
key: '/mobile/mobileFriends/addFriends',
|
||||
icon: renderImgIcon(addFriendIcon)
|
||||
}
|
||||
]
|
||||
@@ -251,7 +278,9 @@ const addIconHandler = {
|
||||
/**
|
||||
* 选项选择时关闭蒙板
|
||||
*/
|
||||
select: () => {
|
||||
select: (item: string) => {
|
||||
console.log('选择的项:', item)
|
||||
router.push(item)
|
||||
maskHandler.close()
|
||||
},
|
||||
|
||||
@@ -272,12 +301,61 @@ const addIconHandler = {
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const mobileStore = useMobileStore()
|
||||
|
||||
useMitt.on(MittEnum.MSG_BOX_SHOW, (event: any) => {
|
||||
mobileStore.updateCurrentChatRoom(event.item)
|
||||
})
|
||||
|
||||
const { handleMsgClick } = useMessage()
|
||||
|
||||
const intoRoom = (item: any) => {
|
||||
handleMsgClick(item)
|
||||
router.push(`/mobile/chatRoom/chatMain/${encodeURIComponent(item.name)}`)
|
||||
console.log('进入页面', item)
|
||||
setTimeout(() => {
|
||||
router.push(`/mobile/chatRoom/chatMain`)
|
||||
console.log('进入页面', item)
|
||||
}, 0)
|
||||
}
|
||||
const toSimpleBio = () => {
|
||||
// 切成你想要的离场动画
|
||||
router.push('/mobile/mobileMy/simpleBio')
|
||||
}
|
||||
|
||||
// 锁滚动(和蒙板一样)
|
||||
const lockScroll = () => {
|
||||
console.log('锁定触发')
|
||||
const scrollEl = document.querySelector('.flex-1.overflow-auto') as HTMLElement
|
||||
if (scrollEl) {
|
||||
scrollEl.style.overflow = 'hidden'
|
||||
}
|
||||
}
|
||||
|
||||
const unlockScroll = () => {
|
||||
console.log('锁定解除')
|
||||
const scrollEl = document.querySelector('.flex-1.overflow-auto') as HTMLElement
|
||||
if (scrollEl) {
|
||||
scrollEl.style.overflow = 'auto'
|
||||
}
|
||||
}
|
||||
|
||||
// 键盘蒙板显示状态
|
||||
const showKeyboardMask = ref(false)
|
||||
|
||||
const openKeyboardMask = () => {
|
||||
showKeyboardMask.value = true
|
||||
document.body.style.overflow = 'hidden'
|
||||
document.body.style.position = 'fixed'
|
||||
}
|
||||
|
||||
const closeKeyboardMask = () => {
|
||||
showKeyboardMask.value = false
|
||||
document.body.style.overflow = ''
|
||||
document.body.style.position = ''
|
||||
// 让 input 失焦
|
||||
const activeEl = document.activeElement as HTMLElement
|
||||
if (activeEl && typeof activeEl.blur === 'function') {
|
||||
activeEl.blur()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -285,4 +363,18 @@ const intoRoom = (item: any) => {
|
||||
// .message-item {
|
||||
// @apply flex justify-around items-center p-3 hover:bg-#DEEDE7 hover:rounded-10px transition-colors cursor-pointer;
|
||||
// }
|
||||
|
||||
.keyboard-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: transparent; // 透明背景
|
||||
z-index: 1400; // 低于 Naive 弹层,高于页面内容
|
||||
pointer-events: auto; // 确保能接收事件
|
||||
touch-action: none; // 禁止滚动
|
||||
}
|
||||
|
||||
::deep(#search) {
|
||||
position: relative;
|
||||
z-index: 1500; // 高于键盘蒙层,低于 Naive 弹层
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -43,8 +43,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { emit } from '@tauri-apps/api/event'
|
||||
import { info } from '@tauri-apps/plugin-log'
|
||||
import { NInput, NSelect, NSwitch } from 'naive-ui'
|
||||
|
||||
import { reactive } from 'vue'
|
||||
import { EventEnum, TauriCommand } from '~/src/enums'
|
||||
import router from '~/src/router'
|
||||
import { useGlobalStore } from '~/src/stores/global'
|
||||
import { invokeSilently } from '~/src/utils/TauriInvokeHandler'
|
||||
|
||||
// 定义设置项
|
||||
const settings = reactive([
|
||||
@@ -82,9 +89,37 @@ const settings = reactive([
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const globalStore = useGlobalStore()
|
||||
const { isTrayMenuShow } = storeToRefs(globalStore)
|
||||
const dialog = useDialog()
|
||||
// 退出登录逻辑
|
||||
function handleLogout() {}
|
||||
async function handleLogout() {
|
||||
dialog.error({
|
||||
title: '提示',
|
||||
content: '确定要退出登录吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
info('登出账号')
|
||||
isTrayMenuShow.value = false
|
||||
try {
|
||||
// ws 退出连接
|
||||
await invokeSilently('ws_disconnect')
|
||||
await invokeSilently(TauriCommand.REMOVE_TOKENS)
|
||||
await invokeSilently(TauriCommand.UPDATE_USER_LAST_OPT_TIME)
|
||||
// 发送登出事件
|
||||
|
||||
await emit(EventEnum.LOGOUT)
|
||||
router.push('/mobile/login')
|
||||
} catch (error) {
|
||||
console.error('创建登录窗口失败:', error)
|
||||
}
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
console.log('用户点击了取消')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 你可以根据需要导出或操作 settings 数据
|
||||
</script>
|
||||
|
||||
127
src/mobile/views/my/SimpleBio.vue
Normal file
127
src/mobile/views/my/SimpleBio.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div class="flex flex-1">
|
||||
<img src="@/assets/mobile/chat-home/background.webp" class="w-100% absolute top-0 z-0" alt="hula" />
|
||||
<AutoFixHeightPage :show-footer="false">
|
||||
<template #container="{ height }">
|
||||
<div :style="{ height: height + 'px' }" class="z-2 flex flex-col absolute overflow-auto min-h-70vh w-full">
|
||||
<div class="flex flex-col flex-1 p-20px gap-20px">
|
||||
<div class="flex items-center">
|
||||
<div class="py-15px flex gap-10px w-full items-center justify-end">
|
||||
<div class="bg-#E7EFE6 flex flex-wrap ps-2 items-center rounded-full gap-1 w-50px h-24px">
|
||||
<span class="w-12px h-12px rounded-15px bg-#079669"></span>
|
||||
<span class="text-bold-style" style="font-size: 12px; color: #373838">在线</span>
|
||||
</div>
|
||||
<svg @click="toSettings" class="iconpark-icon h-32px w-32px block"><use href="#wode-shezhi"></use></svg>
|
||||
<svg @click="toScanQRCode" class="iconpark-icon h-32px w-32px block"><use href="#saoma"></use></svg>
|
||||
<svg @click="handleBack" class="w-32px h-32px iconpark-icon"><use href="#right"></use></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex shadow bg-white w-full rounded-lg items-center">
|
||||
<div class="p-10px flex w-full flex-wrap gap-10px">
|
||||
<div
|
||||
class="rounded-full shadow self-center h-auto transition-transform duration-300 ease-in-out origin-top"
|
||||
style="transform: scale(1) translateY(0)">
|
||||
<n-avatar
|
||||
:size="74"
|
||||
:src="AvatarUtils.getAvatarUrl(userStore.userInfo.avatar!)"
|
||||
fallback-src="/logo.png"
|
||||
round />
|
||||
</div>
|
||||
|
||||
<div @click="toMyInfo" class="flex flex-col flex-1 py-10px">
|
||||
<div class="font-bold text-18px text-#373838">{{ userStore.userInfo.name }}</div>
|
||||
<div class="mt-2 text-bold-style line-height-22px line-clamp-2">
|
||||
一段自我描述,添加性别/地区/工作或学校 不定期更新的日常不定期更新的日常不定期更新的日常
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div @click="toMyInfo" class="flex items-center justify-end">
|
||||
<svg @click="handleBack" class="w-24px text-gray h-24px iconpark-icon"><use href="#right"></use></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col w-full bg-white rounded-lg flex-1">
|
||||
<div
|
||||
v-for="item in options"
|
||||
:key="item.label"
|
||||
class="flex flex-wrap items-center gap-15px p-[15px_10px_5px_10px]">
|
||||
<div>
|
||||
<svg class="iconpark-icon w-30px h-30px"><use :href="'#' + item.icon"></use></svg>
|
||||
</div>
|
||||
<div class="flex-1 text-14px">{{ item.label }}</div>
|
||||
<div>
|
||||
<svg @click="handleBack" class="w-20px text-gray h-20px iconpark-icon"><use href="#right"></use></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</AutoFixHeightPage>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import router from '@/router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const options = ref([
|
||||
{
|
||||
icon: 'xiangce',
|
||||
label: '相册'
|
||||
},
|
||||
{
|
||||
icon: 'shoucang',
|
||||
label: '收藏'
|
||||
},
|
||||
{
|
||||
icon: 'wenjian',
|
||||
label: '文件'
|
||||
},
|
||||
{
|
||||
icon: 'gexingzhuangban',
|
||||
label: '个性装扮'
|
||||
}
|
||||
])
|
||||
|
||||
const toSettings = () => {
|
||||
router.push('/mobile/mobileMy/settings')
|
||||
}
|
||||
|
||||
const toScanQRCode = () => {
|
||||
router.push('/mobile/mobileMy/scanQRcode')
|
||||
}
|
||||
|
||||
const toMyInfo = () => {
|
||||
router.push('/mobile/my')
|
||||
}
|
||||
|
||||
const handleBack = async () => {
|
||||
// const result = await invoke('plugin:hula|ping', {
|
||||
// payload: { value: 'hello world' }
|
||||
// })
|
||||
// console.log('插件测试结果:', result)
|
||||
|
||||
// TODO 返回上一页
|
||||
router.back()
|
||||
console.log('返回')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$text-font-size-base: 14px;
|
||||
|
||||
$font-family-system: -apple-system, BlinkMacSystemFont;
|
||||
$font-family-windows: 'Segoe UI', 'Microsoft YaHei';
|
||||
$font-family-chinese: 'PingFang SC', 'Hiragino Sans GB';
|
||||
$font-family-sans: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
|
||||
.text-bold-style {
|
||||
font-size: 14px;
|
||||
font-family: $font-family-system, $font-family-windows, $font-family-sans;
|
||||
color: #757775;
|
||||
}
|
||||
</style>
|
||||
@@ -42,11 +42,11 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import CommunityContent from '@/mobile/components/community/CommunityContent.vue'
|
||||
import CommunityTab from '@/mobile/components/community/CommunityTab.vue'
|
||||
import PersonalInfo from '@/mobile/components/my/PersonalInfo.vue'
|
||||
import Settings from '@/mobile/components/my/Settings.vue'
|
||||
import SafeAreaPlaceholder from '@/mobile/components/placeholders/SafeAreaPlaceholder.vue'
|
||||
import CommunityContent from '#/components/community/CommunityContent.vue'
|
||||
import CommunityTab from '#/components/community/CommunityTab.vue'
|
||||
import PersonalInfo from '#/components/my/PersonalInfo.vue'
|
||||
import Settings from '#/components/my/Settings.vue'
|
||||
import SafeAreaPlaceholder from '#/components/placeholders/SafeAreaPlaceholder.vue'
|
||||
import router from '@/router'
|
||||
|
||||
const toPublishCommunity = () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import {
|
||||
createRouter,
|
||||
@@ -6,205 +7,213 @@ import {
|
||||
type RouteLocationNormalized,
|
||||
type RouteRecordRaw
|
||||
} from 'vue-router'
|
||||
import ChatRoomLayout from '#/layout/chat-room/ChatRoomLayout.vue'
|
||||
import NoticeLayout from '#/layout/chat-room/NoticeLayout.vue'
|
||||
import FriendsLayout from '#/layout/friends/FriendsLayout.vue'
|
||||
import MobileHome from '#/layout/index.vue'
|
||||
import MyLayout from '#/layout/my/MyLayout.vue'
|
||||
import MobileLogin from '#/login.vue'
|
||||
import ChatMain from '#/views/chat-room/ChatMain.vue'
|
||||
import ChatSetting from '#/views/chat-room/ChatSetting.vue'
|
||||
import NoticeDetail from '#/views/chat-room/notice/NoticeDetail.vue'
|
||||
import NoticeEdit from '#/views/chat-room/notice/NoticeEdit.vue'
|
||||
import NoticeList from '#/views/chat-room/notice/NoticeList.vue'
|
||||
import MobileCommunity from '#/views/community/index.vue'
|
||||
import AddFriends from '#/views/friends/AddFriends.vue'
|
||||
import MobileFriendPage from '#/views/friends/index.vue'
|
||||
import StartGroupChat from '#/views/friends/StartGroupChat.vue'
|
||||
import MobileMessagePage from '#/views/message/index.vue'
|
||||
import EditBio from '#/views/my/EditBio.vue'
|
||||
import EditBirthday from '#/views/my/EditBirthday.vue'
|
||||
import EditProfile from '#/views/my/EditProfile.vue'
|
||||
import MobileMy from '#/views/my/index.vue'
|
||||
import MobileQRCode from '#/views/my/MobileQRCode.vue'
|
||||
import MobileSettings from '#/views/my/MobileSettings.vue'
|
||||
import MyMessages from '#/views/my/MyMessages.vue'
|
||||
import MyQRCode from '#/views/my/MyQRCode.vue'
|
||||
import PublishCommunity from '#/views/my/PublishCommunity.vue'
|
||||
import Share from '#/views/my/Share.vue'
|
||||
import SimpleBio from '#/views/my/SimpleBio.vue'
|
||||
import { TauriCommand } from '@/enums'
|
||||
|
||||
/**! 创建窗口后再跳转页面就会导致样式没有生效所以不能使用懒加载路由的方式,有些页面需要快速响应的就不需要懒加载 */
|
||||
const { BASE_URL } = import.meta.env
|
||||
|
||||
const isMobile = type() === 'ios' || type() === 'android'
|
||||
// 移动端路由配置 - 保持原始的非懒加载设计以避免样式和性能问题
|
||||
const getMobileRoutes = async (): Promise<Array<RouteRecordRaw>> => {
|
||||
// 动态导入移动端组件,但立即解析以避免懒加载问题
|
||||
const [
|
||||
{ default: ChatRoomLayout },
|
||||
{ default: NoticeLayout },
|
||||
{ default: MobileHome },
|
||||
{ default: MyLayout },
|
||||
{ default: MobileLogin },
|
||||
{ default: ChatMain },
|
||||
{ default: ChatSetting },
|
||||
{ default: NoticeDetail },
|
||||
{ default: NoticeEdit },
|
||||
{ default: NoticeList },
|
||||
{ default: MobileCommunity },
|
||||
{ default: MobileFriendPage },
|
||||
{ default: MobileMessagePage },
|
||||
{ default: EditBio },
|
||||
{ default: EditBirthday },
|
||||
{ default: EditProfile },
|
||||
{ default: MobileMy },
|
||||
{ default: MobileQRCode },
|
||||
{ default: MobileSettings },
|
||||
{ default: MyMessages },
|
||||
{ default: MyQRCode },
|
||||
{ default: PublishCommunity },
|
||||
{ default: Share }
|
||||
] = await Promise.all([
|
||||
import('@/mobile/layout/chat-room/ChatRoomLayout.vue'),
|
||||
import('@/mobile/layout/chat-room/NoticeLayout.vue'),
|
||||
import('@/mobile/layout/index.vue'),
|
||||
import('@/mobile/layout/my/MyLayout.vue'),
|
||||
import('@/mobile/login.vue'),
|
||||
import('@/mobile/views/chat-room/ChatMain.vue'),
|
||||
import('@/mobile/views/chat-room/ChatSetting.vue'),
|
||||
import('@/mobile/views/chat-room/notice/NoticeDetail.vue'),
|
||||
import('@/mobile/views/chat-room/notice/NoticeEdit.vue'),
|
||||
import('@/mobile/views/chat-room/notice/NoticeList.vue'),
|
||||
import('@/mobile/views/community/index.vue'),
|
||||
import('@/mobile/views/friends/index.vue'),
|
||||
import('@/mobile/views/message/index.vue'),
|
||||
import('@/mobile/views/my/EditBio.vue'),
|
||||
import('@/mobile/views/my/EditBirthday.vue'),
|
||||
import('@/mobile/views/my/EditProfile.vue'),
|
||||
import('@/mobile/views/my/index.vue'),
|
||||
import('@/mobile/views/my/MobileQRCode.vue'),
|
||||
import('@/mobile/views/my/MobileSettings.vue'),
|
||||
import('@/mobile/views/my/MyMessages.vue'),
|
||||
import('@/mobile/views/my/MyQRCode.vue'),
|
||||
import('@/mobile/views/my/PublishCommunity.vue'),
|
||||
import('@/mobile/views/my/Share.vue')
|
||||
])
|
||||
|
||||
return [
|
||||
{
|
||||
path: '/mobile/login',
|
||||
name: 'mobileLogin',
|
||||
component: MobileLogin
|
||||
},
|
||||
{
|
||||
path: '/mobile/chatRoom',
|
||||
name: 'mobileChatRoom',
|
||||
component: ChatRoomLayout, // 这里不能懒加载,不然会出现布局问题
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'mobileChatRoomDefault',
|
||||
redirect: '/mobile/chatRoom/chatMain'
|
||||
},
|
||||
{
|
||||
path: 'chatMain/:roomName',
|
||||
name: 'mobileChatMain',
|
||||
component: ChatMain
|
||||
},
|
||||
{
|
||||
path: 'setting',
|
||||
name: 'mobileChatSetting',
|
||||
component: ChatSetting
|
||||
},
|
||||
{
|
||||
path: 'notice',
|
||||
name: 'mobileChatNotice',
|
||||
component: NoticeLayout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'mobileChatNoticeList',
|
||||
component: NoticeList
|
||||
},
|
||||
{
|
||||
path: 'edit/:noticeId',
|
||||
name: 'mobileChatNoticeEdit',
|
||||
component: NoticeEdit
|
||||
},
|
||||
{
|
||||
path: 'detail/:noticeId',
|
||||
name: 'mobileChatNoticeDetail',
|
||||
component: NoticeDetail
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/mobile/home',
|
||||
name: 'mobileHome',
|
||||
component: MobileHome,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'mobileMessage',
|
||||
redirect: '/mobile/message'
|
||||
},
|
||||
{
|
||||
path: '/mobile/message',
|
||||
name: 'mobileMessage',
|
||||
component: MobileMessagePage
|
||||
},
|
||||
{
|
||||
path: '/mobile/friends',
|
||||
name: 'mobileFriends',
|
||||
component: MobileFriendPage
|
||||
},
|
||||
{
|
||||
path: '/mobile/community',
|
||||
name: 'mobileCommunity',
|
||||
component: MobileCommunity
|
||||
},
|
||||
{
|
||||
path: '/mobile/my',
|
||||
name: 'mobileMy',
|
||||
component: MobileMy
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/mobile/mobileMy',
|
||||
name: 'mobileMyLayout',
|
||||
component: MyLayout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'mobileMyDefault',
|
||||
redirect: '/mobile/mobileMy/editProfile'
|
||||
},
|
||||
{
|
||||
path: 'editProfile',
|
||||
name: 'mobileEditProfile',
|
||||
component: EditProfile
|
||||
},
|
||||
{
|
||||
path: 'myMessages',
|
||||
name: 'mobileMyMessages',
|
||||
component: MyMessages
|
||||
},
|
||||
{
|
||||
path: 'editBio',
|
||||
name: 'mobileEditBio',
|
||||
component: EditBio
|
||||
},
|
||||
{
|
||||
path: 'editBirthday',
|
||||
name: 'mobileEditBirthday',
|
||||
component: EditBirthday
|
||||
},
|
||||
{
|
||||
path: 'publishCommunity',
|
||||
name: 'mobilePublishCommunity',
|
||||
component: PublishCommunity
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'MobileSettings',
|
||||
component: MobileSettings
|
||||
},
|
||||
{
|
||||
path: 'scanQRCode',
|
||||
name: 'mobileQRCode',
|
||||
component: MobileQRCode
|
||||
},
|
||||
{
|
||||
path: 'share',
|
||||
name: 'mobileShare',
|
||||
component: Share
|
||||
},
|
||||
{
|
||||
path: 'myQRCode',
|
||||
name: 'mobileShare',
|
||||
component: MyQRCode
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
// 移动端路由配置 - 使用直接导入避免懒加载问题
|
||||
const getMobileRoutes = (): Array<RouteRecordRaw> => [
|
||||
{
|
||||
path: '/',
|
||||
name: 'mobileRoot',
|
||||
redirect: '/mobile/login'
|
||||
},
|
||||
{
|
||||
path: '/mobile/login',
|
||||
name: 'mobileLogin',
|
||||
component: MobileLogin
|
||||
},
|
||||
{
|
||||
path: '/mobile/chatRoom',
|
||||
name: 'mobileChatRoom',
|
||||
component: ChatRoomLayout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'mobileChatRoomDefault',
|
||||
redirect: '/mobile/chatRoom/chatMain'
|
||||
},
|
||||
{
|
||||
path: 'chatMain',
|
||||
name: 'mobileChatMain',
|
||||
component: ChatMain
|
||||
},
|
||||
{
|
||||
path: 'setting',
|
||||
name: 'mobileChatSetting',
|
||||
component: ChatSetting
|
||||
},
|
||||
{
|
||||
path: 'notice',
|
||||
name: 'mobileChatNotice',
|
||||
component: NoticeLayout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'mobileChatNoticeList',
|
||||
component: NoticeList
|
||||
},
|
||||
{
|
||||
path: 'edit[NO]ticeId',
|
||||
name: 'mobileChatNoticeEdit',
|
||||
component: NoticeEdit
|
||||
},
|
||||
{
|
||||
path: 'detail[NO]ticeId',
|
||||
name: 'mobileChatNoticeDetail',
|
||||
component: NoticeDetail
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/mobile/home',
|
||||
name: 'mobileHome',
|
||||
component: MobileHome,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'mobileHomeDefault',
|
||||
redirect: '/mobile/message'
|
||||
},
|
||||
{
|
||||
path: '/mobile/message',
|
||||
name: 'mobileMessage',
|
||||
component: MobileMessagePage
|
||||
},
|
||||
{
|
||||
path: '/mobile/friends',
|
||||
name: 'mobileFriends',
|
||||
component: MobileFriendPage
|
||||
},
|
||||
{
|
||||
path: '/mobile/community',
|
||||
name: 'mobileCommunity',
|
||||
component: MobileCommunity
|
||||
},
|
||||
{
|
||||
path: '/mobile/my',
|
||||
name: 'mobileMy',
|
||||
component: MobileMy
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/mobile/mobileMy',
|
||||
name: 'mobileMyLayout',
|
||||
component: MyLayout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'mobileMyDefault',
|
||||
redirect: '/mobile/mobileMy/editProfile'
|
||||
},
|
||||
{
|
||||
path: 'editProfile',
|
||||
name: 'mobileEditProfile',
|
||||
component: EditProfile
|
||||
},
|
||||
{
|
||||
path: 'myMessages',
|
||||
name: 'mobileMyMessages',
|
||||
component: MyMessages
|
||||
},
|
||||
{
|
||||
path: 'editBio',
|
||||
name: 'mobileEditBio',
|
||||
component: EditBio
|
||||
},
|
||||
{
|
||||
path: 'editBirthday',
|
||||
name: 'mobileEditBirthday',
|
||||
component: EditBirthday
|
||||
},
|
||||
{
|
||||
path: 'publishCommunity',
|
||||
name: 'mobilePublishCommunity',
|
||||
component: PublishCommunity
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'MobileSettings',
|
||||
component: MobileSettings
|
||||
},
|
||||
{
|
||||
path: 'scanQRCode',
|
||||
name: 'mobileQRCode',
|
||||
component: MobileQRCode
|
||||
},
|
||||
{
|
||||
path: 'share',
|
||||
name: 'mobileShare',
|
||||
component: Share
|
||||
},
|
||||
{
|
||||
path: 'myQRCode',
|
||||
name: 'mobileMyQRCode',
|
||||
component: MyQRCode
|
||||
},
|
||||
{
|
||||
path: 'SimpleBio',
|
||||
name: 'mobileSimpleBio',
|
||||
component: SimpleBio
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/mobile/mobileFriends',
|
||||
name: 'mobileFriendsLayout',
|
||||
component: FriendsLayout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'mobileMyDefault',
|
||||
redirect: '/mobile/mobileFriends/addFriends'
|
||||
},
|
||||
{
|
||||
path: 'addFriends',
|
||||
name: 'mobileAddFriends',
|
||||
component: AddFriends
|
||||
},
|
||||
{
|
||||
path: 'startGroupChat',
|
||||
name: 'mobileStartGroupChat',
|
||||
component: StartGroupChat
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// 桌面端路由配置
|
||||
const getDesktopRoutes = (): Array<RouteRecordRaw> => [
|
||||
@@ -415,47 +424,57 @@ const getCommonRoutes = (): Array<RouteRecordRaw> => [
|
||||
}
|
||||
]
|
||||
|
||||
// 创建基础路由(通用路由 + 桌面端路由,如果是桌面端的话)
|
||||
const baseRoutes: Array<RouteRecordRaw> = [...getCommonRoutes(), ...(!isMobile ? getDesktopRoutes() : [])]
|
||||
// 创建所有路由(通用路由 + 平台特定路由)
|
||||
const getAllRoutes = (): Array<RouteRecordRaw> => {
|
||||
const commonRoutes = getCommonRoutes()
|
||||
if (isMobile) {
|
||||
return [...commonRoutes, ...getMobileRoutes()]
|
||||
} else {
|
||||
return [...commonRoutes, ...getDesktopRoutes()]
|
||||
}
|
||||
}
|
||||
|
||||
// 创建路由
|
||||
const router: any = createRouter({
|
||||
history: createWebHistory(BASE_URL),
|
||||
routes: baseRoutes
|
||||
routes: getAllRoutes()
|
||||
})
|
||||
|
||||
// 移动端异步加载路由
|
||||
if (isMobile) {
|
||||
getMobileRoutes().then((mobileRoutes) => {
|
||||
mobileRoutes.forEach((route) => {
|
||||
router.addRoute(route)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 在创建路由后,添加全局前置守卫
|
||||
// 为解决 “已声明‘to’,但从未读取其值” 的问题,将 to 参数改为下划线开头表示该参数不会被使用
|
||||
router.beforeEach((_to: RouteLocationNormalized, _from: RouteLocationNormalized, next: NavigationGuardNext) => {
|
||||
// 如果是桌面端,直接放行
|
||||
router.beforeEach(async (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
|
||||
console.log('进入了路由:', from, to)
|
||||
// 桌面端直接放行
|
||||
if (!isMobile) {
|
||||
return next()
|
||||
}
|
||||
|
||||
// const token = localStorage.getItem('TOKEN')
|
||||
// const isLoginPage = to.path === '/mobile/login'
|
||||
try {
|
||||
const tokens = await invoke<{ token: string | null; refreshToken: string | null }>(TauriCommand.GET_USER_TOKENS)
|
||||
const isLoginPage = to.path === '/mobile/login'
|
||||
const isLoggedIn = !!(tokens.token && tokens.refreshToken)
|
||||
|
||||
// // 已登录用户访问登录页时重定向到首页
|
||||
// if (isLoginPage && token) {
|
||||
// return next('/mobile/home')
|
||||
// }
|
||||
// 未登录且不是登录页 → 跳转登录
|
||||
if (!isLoggedIn && !isLoginPage) {
|
||||
console.error('没有登录')
|
||||
return next('/mobile/login')
|
||||
}
|
||||
|
||||
// // 未登录用户访问非登录页时重定向到登录页
|
||||
// if (!isLoginPage && !token) {
|
||||
// return next('/mobile/login')
|
||||
// }
|
||||
// 已登录且访问登录页 → 跳转首页
|
||||
if (isLoggedIn && isLoginPage) {
|
||||
return next('/mobile/message')
|
||||
}
|
||||
|
||||
// 其他情况正常放行
|
||||
next()
|
||||
// 其他情况直接放行
|
||||
return next()
|
||||
} catch (error) {
|
||||
console.error('获取token错误:', error)
|
||||
// 出错时也跳转登录页(避免死循环)
|
||||
if (to.path !== '/mobile/login') {
|
||||
return next('/mobile/login')
|
||||
}
|
||||
return next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -286,7 +286,9 @@ class RustWebSocketClient {
|
||||
const listenerIndex = this.listenerController.size
|
||||
this.listenerController.add(
|
||||
await listen('ws-receive-message', (event: any) => {
|
||||
console.log('[测试]收到消息:', event)
|
||||
info(`[ws]收到消息[监听器${listenerIndex}]: ${JSON.stringify(event.payload)}`)
|
||||
// debugger
|
||||
useMitt.emit(WsResponseMessageType.RECEIVE_MESSAGE, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
@@ -301,7 +301,7 @@ export const useChatStore = defineStore(
|
||||
sessionOptions.isLoading = true
|
||||
console.log('获取会话列表')
|
||||
const response: any = await invokeWithErrorHandler(TauriCommand.LIST_CONTACTS, undefined, {
|
||||
customErrorMessage: '获取会话列表失败22',
|
||||
customErrorMessage: '获取会话列表失败',
|
||||
errorType: ErrorType.Network
|
||||
}).catch(() => {
|
||||
sessionOptions.isLoading = false
|
||||
|
||||
@@ -86,6 +86,12 @@ export const useMobileStore = defineStore(StoresEnum.MOBILE, () => {
|
||||
})
|
||||
}
|
||||
|
||||
const currentChatRoom: any = ref({})
|
||||
|
||||
const updateCurrentChatRoom = (item: any) => {
|
||||
currentChatRoom.value = item
|
||||
}
|
||||
|
||||
return {
|
||||
safeArea,
|
||||
updateSafeArea,
|
||||
@@ -95,6 +101,8 @@ export const useMobileStore = defineStore(StoresEnum.MOBILE, () => {
|
||||
isFullScreen,
|
||||
keyboardDetail,
|
||||
updateKeyboardDetail,
|
||||
updateKeyboardState
|
||||
updateKeyboardState,
|
||||
currentChatRoom,
|
||||
updateCurrentChatRoom
|
||||
}
|
||||
})
|
||||
|
||||
6
src/styles/scss/global/mobile.scss
vendored
6
src/styles/scss/global/mobile.scss
vendored
@@ -16,6 +16,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
user-select: none !important;
|
||||
-webkit-user-select: none !important;
|
||||
-webkit-touch-callout: none !important;
|
||||
}
|
||||
|
||||
/* 移动端特定样式 */
|
||||
/* Naive UI 消息组件安全区域适配 */
|
||||
.n-message-container.n-message-container--top {
|
||||
|
||||
1
src/typings/components.d.ts
vendored
1
src/typings/components.d.ts
vendored
@@ -48,6 +48,7 @@ declare module 'vue' {
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup']
|
||||
NCollapse: typeof import('naive-ui')['NCollapse']
|
||||
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
|
||||
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
|
||||
|
||||
Reference in New Issue
Block a user