mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 19:04:23 +08:00
feat: 新增局域网浏览器访问
This commit is contained in:
Generated
+123
-5
@@ -814,14 +814,38 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.20.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core 0.20.11",
|
||||||
|
"darling_macro 0.20.11",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
version = "0.23.0"
|
version = "0.23.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
|
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.23.0",
|
||||||
"darling_macro",
|
"darling_macro 0.23.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.20.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim",
|
||||||
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -837,13 +861,24 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.20.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core 0.20.11",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling_macro"
|
name = "darling_macro"
|
||||||
version = "0.23.0"
|
version = "0.23.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.23.0",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
@@ -880,6 +915,37 @@ dependencies = [
|
|||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
|
||||||
|
dependencies = [
|
||||||
|
"derive_builder_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder_core"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
||||||
|
dependencies = [
|
||||||
|
"darling 0.20.11",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder_macro"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
||||||
|
dependencies = [
|
||||||
|
"derive_builder_core",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_more"
|
name = "derive_more"
|
||||||
version = "0.99.20"
|
version = "0.99.20"
|
||||||
@@ -1532,6 +1598,17 @@ dependencies = [
|
|||||||
"wasip3",
|
"wasip3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getset"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6cf442baaabe4213ce7d1239afc26c039180b6456da2cededa316ae2c8a77a77"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gio"
|
name = "gio"
|
||||||
version = "0.18.4"
|
version = "0.18.4"
|
||||||
@@ -2379,6 +2456,17 @@ version = "0.8.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "local-ip-address"
|
||||||
|
version = "0.6.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aa08fb2b1ec3ea84575e94b489d06d4ce0cbf052d12acd515838f50e3c3d63e3"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"neli",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
@@ -2582,6 +2670,35 @@ dependencies = [
|
|||||||
"jni-sys 0.3.1",
|
"jni-sys 0.3.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "neli"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22f9786d56d972959e1408b6a93be6af13b9c1392036c5c1fafa08a1b0c6ee87"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.0",
|
||||||
|
"byteorder",
|
||||||
|
"derive_builder",
|
||||||
|
"getset",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"neli-proc-macros",
|
||||||
|
"parking_lot",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "neli-proc-macros"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05d8d08c6e98f20a62417478ebf7be8e1425ec9acecc6f63e22da633f6b71609"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"serde",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "new_debug_unreachable"
|
name = "new_debug_unreachable"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@@ -3868,7 +3985,7 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ab9d95d7ed26ad8306352b0d5f05b593222b272790564589790d210aa15caa9e"
|
checksum = "ab9d95d7ed26ad8306352b0d5f05b593222b272790564589790d210aa15caa9e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling 0.23.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -4222,6 +4339,7 @@ dependencies = [
|
|||||||
"dirs",
|
"dirs",
|
||||||
"hex",
|
"hex",
|
||||||
"libc",
|
"libc",
|
||||||
|
"local-ip-address",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
@@ -4437,7 +4555,7 @@ version = "3.18.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65"
|
checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling 0.23.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ parking_lot = { version = "0.12", features = ["send_guard"] }
|
|||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||||
urlencoding = "2.1"
|
urlencoding = "2.1"
|
||||||
|
local-ip-address = "0.6.13"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winreg = "0.52"
|
winreg = "0.52"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -148,6 +148,7 @@ pub fn run() {
|
|||||||
fs_file_exists,
|
fs_file_exists,
|
||||||
fs_open_path,
|
fs_open_path,
|
||||||
http_server_start,
|
http_server_start,
|
||||||
|
http_server_refresh_token,
|
||||||
http_server_stop,
|
http_server_stop,
|
||||||
http_server_status,
|
http_server_status,
|
||||||
mcp_server_start,
|
mcp_server_start,
|
||||||
@@ -174,6 +175,41 @@ pub fn setup_app(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
setup_deep_link(app)?;
|
setup_deep_link(app)?;
|
||||||
|
|
||||||
|
setup_lan_http_server(app)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_lan_http_server(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let handle = app.handle().clone();
|
||||||
|
let state = handle.state::<crate::state::SafeAppState>().inner().clone();
|
||||||
|
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
let enabled = {
|
||||||
|
let state_guard = state.read();
|
||||||
|
let db_conn = state_guard.db.read().clone();
|
||||||
|
let mut settings = state_guard.settings.write();
|
||||||
|
settings.attach_db(db_conn);
|
||||||
|
if settings.initialize().await.is_err() {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
matches!(
|
||||||
|
settings.get_value(SettingsKey::LanAccessEnabled),
|
||||||
|
SettingsValue::Boolean(true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if enabled {
|
||||||
|
if let Err(error) =
|
||||||
|
crate::commands::http_server_start_from_settings(handle.clone(), state.clone())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!("Failed to start LAN server from settings: {}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub struct SettingsSpec {
|
|||||||
pub pg_connection_string: String,
|
pub pg_connection_string: String,
|
||||||
pub pg_connection_status: JsonValue,
|
pub pg_connection_status: JsonValue,
|
||||||
pub mobile_bottom_nav_items: JsonValue,
|
pub mobile_bottom_nav_items: JsonValue,
|
||||||
|
pub lan_access_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SettingsSpec {
|
impl Default for SettingsSpec {
|
||||||
@@ -51,6 +52,7 @@ impl Default for SettingsSpec {
|
|||||||
"reasons",
|
"reasons",
|
||||||
"settings"
|
"settings"
|
||||||
]),
|
]),
|
||||||
|
lan_access_enabled: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,6 +74,7 @@ pub enum SettingsKey {
|
|||||||
PgConnectionString,
|
PgConnectionString,
|
||||||
PgConnectionStatus,
|
PgConnectionStatus,
|
||||||
MobileBottomNavItems,
|
MobileBottomNavItems,
|
||||||
|
LanAccessEnabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SettingsKey {
|
impl SettingsKey {
|
||||||
@@ -92,6 +95,7 @@ impl SettingsKey {
|
|||||||
SettingsKey::PgConnectionString => "pg_connection_string",
|
SettingsKey::PgConnectionString => "pg_connection_string",
|
||||||
SettingsKey::PgConnectionStatus => "pg_connection_status",
|
SettingsKey::PgConnectionStatus => "pg_connection_status",
|
||||||
SettingsKey::MobileBottomNavItems => "mobile_bottom_nav_items",
|
SettingsKey::MobileBottomNavItems => "mobile_bottom_nav_items",
|
||||||
|
SettingsKey::LanAccessEnabled => "lan_access_enabled",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +116,7 @@ impl SettingsKey {
|
|||||||
"pg_connection_string" => Some(SettingsKey::PgConnectionString),
|
"pg_connection_string" => Some(SettingsKey::PgConnectionString),
|
||||||
"pg_connection_status" => Some(SettingsKey::PgConnectionStatus),
|
"pg_connection_status" => Some(SettingsKey::PgConnectionStatus),
|
||||||
"mobile_bottom_nav_items" => Some(SettingsKey::MobileBottomNavItems),
|
"mobile_bottom_nav_items" => Some(SettingsKey::MobileBottomNavItems),
|
||||||
|
"lan_access_enabled" => Some(SettingsKey::LanAccessEnabled),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -475,6 +480,16 @@ impl SettingsService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
defs.insert(
|
||||||
|
SettingsKey::LanAccessEnabled,
|
||||||
|
SettingDefinition {
|
||||||
|
kind: SettingValueKind::Boolean,
|
||||||
|
default_value: SettingsValue::Boolean(false),
|
||||||
|
write_permission: PermissionRequirement::Admin,
|
||||||
|
validate: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
defs
|
defs
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,6 +651,10 @@ impl SettingsService {
|
|||||||
"settings"
|
"settings"
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
lan_access_enabled: match self.get_value(SettingsKey::LanAccessEnabled) {
|
||||||
|
SettingsValue::Boolean(b) => b,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import React, { Suspense, lazy, useCallback, useEffect, useRef, useState } from "react"
|
import React, { Suspense, lazy, useCallback, useEffect, useRef, useState } from "react"
|
||||||
import { Layout, Space, Button, Tag, Spin, Avatar, Popover, Progress } from "antd"
|
import { Layout, Space, Button, Tag, Spin, Avatar, Popover, Progress, Input, QRCode } from "antd"
|
||||||
import {
|
import {
|
||||||
MenuFoldOutlined,
|
MenuFoldOutlined,
|
||||||
MenuUnfoldOutlined,
|
MenuUnfoldOutlined,
|
||||||
LeftOutlined,
|
LeftOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
|
LinkOutlined,
|
||||||
|
CopyOutlined,
|
||||||
} from "@ant-design/icons"
|
} from "@ant-design/icons"
|
||||||
import { Routes, Route, Navigate, useLocation, useNavigate } from "react-router-dom"
|
import { Routes, Route, Navigate, useLocation, useNavigate } from "react-router-dom"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
@@ -84,6 +86,13 @@ interface HeaderStorageUsage {
|
|||||||
file_count: number
|
file_count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LanShareUrl {
|
||||||
|
ip: string
|
||||||
|
url: string
|
||||||
|
is_private?: boolean
|
||||||
|
is_192_168?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export function ContentArea({
|
export function ContentArea({
|
||||||
permission,
|
permission,
|
||||||
oauthUserName,
|
oauthUserName,
|
||||||
@@ -107,12 +116,13 @@ export function ContentArea({
|
|||||||
bottomInset = 0,
|
bottomInset = 0,
|
||||||
}: ContentAreaProps): React.JSX.Element {
|
}: ContentAreaProps): React.JSX.Element {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const isLanBrowser = Boolean((window as any).__SECSCORE_LAN__)
|
||||||
const isMacOS =
|
const isMacOS =
|
||||||
typeof navigator !== "undefined" &&
|
typeof navigator !== "undefined" &&
|
||||||
/mac/i.test(navigator.userAgent) &&
|
/mac/i.test(navigator.userAgent) &&
|
||||||
!/iphone|ipad|ipod|android/i.test(navigator.userAgent)
|
!/iphone|ipad|ipod|android/i.test(navigator.userAgent)
|
||||||
// macOS 使用原生红绿灯,侧栏隐藏(沉浸/竖屏)时顶部左侧需为红绿灯留白
|
// macOS 使用原生红绿灯,侧栏隐藏(沉浸/竖屏)时顶部左侧需为红绿灯留白
|
||||||
const macTrafficLightsInset = isMacOS && (isPortraitMode || immersiveMode)
|
const macTrafficLightsInset = !isLanBrowser && isMacOS && (isPortraitMode || immersiveMode)
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const isSubPage = location.pathname !== "/" && !location.pathname.startsWith("/home")
|
const isSubPage = location.pathname !== "/" && !location.pathname.startsWith("/home")
|
||||||
@@ -200,7 +210,21 @@ export function ContentArea({
|
|||||||
})
|
})
|
||||||
const [copiedUserId, setCopiedUserId] = useState(false)
|
const [copiedUserId, setCopiedUserId] = useState(false)
|
||||||
const [logoutLoading, setLogoutLoading] = useState(false)
|
const [logoutLoading, setLogoutLoading] = useState(false)
|
||||||
|
const [lanPopoverOpen, setLanPopoverOpen] = useState(false)
|
||||||
|
const [lanLoading, setLanLoading] = useState(false)
|
||||||
|
const [lanCopied, setLanCopied] = useState(false)
|
||||||
|
const [selectedLanShareUrl, setSelectedLanShareUrl] = useState<string | null>(null)
|
||||||
|
const [lanStatus, setLanStatus] = useState<{
|
||||||
|
is_running: boolean
|
||||||
|
url?: string | null
|
||||||
|
api_url?: string | null
|
||||||
|
share_url?: string | null
|
||||||
|
share_urls?: LanShareUrl[]
|
||||||
|
token?: string | null
|
||||||
|
config?: any
|
||||||
|
} | null>(null)
|
||||||
const copyResetTimerRef = useRef<number | null>(null)
|
const copyResetTimerRef = useRef<number | null>(null)
|
||||||
|
const lanCopyResetTimerRef = useRef<number | null>(null)
|
||||||
const hasOAuthSession = Boolean(oauthUserName && oauthUserName.trim())
|
const hasOAuthSession = Boolean(oauthUserName && oauthUserName.trim())
|
||||||
const formattedLastSyncTime = (() => {
|
const formattedLastSyncTime = (() => {
|
||||||
if (!lastSyncTime) return "暂无"
|
if (!lastSyncTime) return "暂无"
|
||||||
@@ -290,6 +314,10 @@ export function ContentArea({
|
|||||||
window.clearTimeout(copyResetTimerRef.current)
|
window.clearTimeout(copyResetTimerRef.current)
|
||||||
copyResetTimerRef.current = null
|
copyResetTimerRef.current = null
|
||||||
}
|
}
|
||||||
|
if (lanCopyResetTimerRef.current) {
|
||||||
|
window.clearTimeout(lanCopyResetTimerRef.current)
|
||||||
|
lanCopyResetTimerRef.current = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -329,6 +357,127 @@ export function ContentArea({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeLanStatus = (data: any) => {
|
||||||
|
if (!data) return null
|
||||||
|
const shareUrls: LanShareUrl[] = Array.isArray(data.share_urls || data.shareUrls)
|
||||||
|
? (data.share_urls || data.shareUrls)
|
||||||
|
.map((item: any) => ({
|
||||||
|
ip: String(item.ip || ""),
|
||||||
|
url: String(item.url || ""),
|
||||||
|
is_private: Boolean(item.is_private ?? item.isPrivate),
|
||||||
|
is_192_168: Boolean(item.is_192_168 ?? item.is192168),
|
||||||
|
}))
|
||||||
|
.filter((item: LanShareUrl) => item.ip && item.url)
|
||||||
|
: []
|
||||||
|
const primaryShareUrl = data.share_url || data.shareUrl || shareUrls[0]?.url || null
|
||||||
|
return {
|
||||||
|
is_running: Boolean(data.is_running ?? data.isRunning),
|
||||||
|
url: data.url || null,
|
||||||
|
api_url: data.api_url || data.apiUrl || null,
|
||||||
|
share_url: primaryShareUrl,
|
||||||
|
share_urls:
|
||||||
|
shareUrls.length > 0
|
||||||
|
? shareUrls
|
||||||
|
: primaryShareUrl
|
||||||
|
? [{ ip: "", url: primaryShareUrl }]
|
||||||
|
: [],
|
||||||
|
token: data.token || null,
|
||||||
|
config: data.config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyLanStatus = (nextStatus: ReturnType<typeof normalizeLanStatus>) => {
|
||||||
|
setLanStatus(nextStatus)
|
||||||
|
setSelectedLanShareUrl((prev) => {
|
||||||
|
const urls = nextStatus?.share_urls || []
|
||||||
|
if (prev && urls.some((item) => item.url === prev)) return prev
|
||||||
|
return urls[0]?.url || nextStatus?.share_url || null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshLanStatus = useCallback(async (refreshToken: boolean) => {
|
||||||
|
const api = (window as any).api
|
||||||
|
if (!api?.httpServerStatus) return
|
||||||
|
setLanLoading(true)
|
||||||
|
try {
|
||||||
|
const statusRes = await api.httpServerStatus()
|
||||||
|
const status = normalizeLanStatus(statusRes?.data)
|
||||||
|
if (refreshToken && status?.is_running && api.httpServerRefreshToken) {
|
||||||
|
const refreshRes = await api.httpServerRefreshToken()
|
||||||
|
if (refreshRes?.success) {
|
||||||
|
applyLanStatus(normalizeLanStatus({ ...refreshRes.data, is_running: true }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
applyLanStatus(status)
|
||||||
|
} finally {
|
||||||
|
setLanLoading(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleLanPopoverOpenChange = (open: boolean) => {
|
||||||
|
setLanPopoverOpen(open)
|
||||||
|
if (open) {
|
||||||
|
void refreshLanStatus(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startLanAccess = async () => {
|
||||||
|
const api = (window as any).api
|
||||||
|
if (!api?.httpServerStart) return
|
||||||
|
setLanLoading(true)
|
||||||
|
try {
|
||||||
|
const res = await api.httpServerStart({
|
||||||
|
host: "0.0.0.0",
|
||||||
|
port: 45739,
|
||||||
|
api_port: 45740,
|
||||||
|
})
|
||||||
|
if (res?.success) {
|
||||||
|
if (api.setSetting) {
|
||||||
|
await api.setSetting("lan_access_enabled", true)
|
||||||
|
}
|
||||||
|
applyLanStatus(normalizeLanStatus({ ...res.data, is_running: true }))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLanLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopLanAccess = async () => {
|
||||||
|
const api = (window as any).api
|
||||||
|
if (!api?.httpServerStop) return
|
||||||
|
setLanLoading(true)
|
||||||
|
try {
|
||||||
|
const res = await api.httpServerStop()
|
||||||
|
if (res?.success) {
|
||||||
|
if (api.setSetting) {
|
||||||
|
await api.setSetting("lan_access_enabled", false)
|
||||||
|
}
|
||||||
|
setLanStatus((prev) => ({ ...(prev || { is_running: false }), is_running: false }))
|
||||||
|
setSelectedLanShareUrl(null)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLanLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyLanLink = async (link = selectedLanShareUrl || lanStatus?.share_url || "") => {
|
||||||
|
if (!link) return
|
||||||
|
await navigator.clipboard.writeText(link)
|
||||||
|
setLanCopied(true)
|
||||||
|
if (lanCopyResetTimerRef.current) {
|
||||||
|
window.clearTimeout(lanCopyResetTimerRef.current)
|
||||||
|
}
|
||||||
|
lanCopyResetTimerRef.current = window.setTimeout(() => {
|
||||||
|
setLanCopied(false)
|
||||||
|
lanCopyResetTimerRef.current = null
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
const lanShareUrls = lanStatus?.share_urls || []
|
||||||
|
const activeLanShareUrl =
|
||||||
|
selectedLanShareUrl || lanStatus?.share_url || lanShareUrls[0]?.url || ""
|
||||||
|
|
||||||
const profilePopoverContent = (
|
const profilePopoverContent = (
|
||||||
<div style={{ width: "260px", display: "flex", flexDirection: "column", gap: "10px" }}>
|
<div style={{ width: "260px", display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||||
<div
|
<div
|
||||||
@@ -408,6 +557,134 @@ export function ContentArea({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const lanPopoverContent = (
|
||||||
|
<div style={{ width: "300px", display: "flex", flexDirection: "column", gap: "12px" }}>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||||
|
<span style={{ fontSize: "13px", fontWeight: 600, color: "var(--ss-text-main)" }}>
|
||||||
|
局域网访问
|
||||||
|
</span>
|
||||||
|
<Tag color={lanStatus?.is_running ? "success" : "default"}>
|
||||||
|
{lanStatus?.is_running ? "已启用" : "未启用"}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!lanStatus?.is_running ? (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
icon={<LinkOutlined />}
|
||||||
|
loading={lanLoading}
|
||||||
|
disabled={permission !== "admin"}
|
||||||
|
onClick={startLanAccess}
|
||||||
|
>
|
||||||
|
启用局域网访问
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
readOnly
|
||||||
|
value={activeLanShareUrl}
|
||||||
|
suffix={
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<CopyOutlined />}
|
||||||
|
onClick={() => copyLanLink()}
|
||||||
|
disabled={!activeLanShareUrl}
|
||||||
|
>
|
||||||
|
{lanCopied ? "已复制" : "复制"}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{activeLanShareUrl && (
|
||||||
|
<div style={{ display: "flex", justifyContent: "center" }}>
|
||||||
|
<QRCode value={activeLanShareUrl} size={180} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{lanShareUrls.length > 0 && (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
||||||
|
{lanShareUrls.map((item, index) => {
|
||||||
|
const selected = item.url === activeLanShareUrl
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${item.ip}-${item.url}`}
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr auto auto",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "6px",
|
||||||
|
padding: "6px 8px",
|
||||||
|
border: selected
|
||||||
|
? "1px solid var(--ant-color-primary)"
|
||||||
|
: "1px solid var(--ss-border-color)",
|
||||||
|
borderRadius: "8px",
|
||||||
|
background: selected
|
||||||
|
? "color-mix(in srgb, var(--ant-color-primary) 8%, transparent)"
|
||||||
|
: "transparent",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ minWidth: 0 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--ss-text-main)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.ip || `地址 ${index + 1}`}
|
||||||
|
{index === 0 && (
|
||||||
|
<Tag color="blue" style={{ marginLeft: "6px" }}>
|
||||||
|
默认
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
{item.is_192_168 && (
|
||||||
|
<Tag color="green" style={{ marginLeft: "4px" }}>
|
||||||
|
192.168
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
title={item.url}
|
||||||
|
style={{
|
||||||
|
fontSize: "11px",
|
||||||
|
color: "var(--ss-text-secondary)",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.url}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button size="small" onClick={() => setSelectedLanShareUrl(item.url)}>
|
||||||
|
二维码
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<CopyOutlined />}
|
||||||
|
onClick={() => copyLanLink(item.url)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ fontSize: "12px", color: "var(--ss-text-secondary)" }}>
|
||||||
|
每次打开此弹窗都会刷新 token,之前复制的链接会失效。
|
||||||
|
</div>
|
||||||
|
<Space>
|
||||||
|
<Button size="small" loading={lanLoading} onClick={() => refreshLanStatus(true)}>
|
||||||
|
刷新链接
|
||||||
|
</Button>
|
||||||
|
<Button size="small" danger loading={lanLoading} onClick={stopLanAccess}>
|
||||||
|
停用
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
style={{
|
style={{
|
||||||
@@ -549,7 +826,23 @@ export function ContentArea({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Space size="small">
|
<Space size="small">
|
||||||
{(immersiveMode || (isHomePage && !isMobileDevice)) && (
|
{!isLanBrowser && (
|
||||||
|
<Popover
|
||||||
|
trigger="click"
|
||||||
|
placement="bottomRight"
|
||||||
|
open={lanPopoverOpen}
|
||||||
|
onOpenChange={handleLanPopoverOpenChange}
|
||||||
|
content={lanPopoverContent}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<LinkOutlined />}
|
||||||
|
title="局域网访问"
|
||||||
|
disabled={permission !== "admin"}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
{!isLanBrowser && (immersiveMode || (isHomePage && !isMobileDevice)) && (
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<SettingOutlined />}
|
icon={<SettingOutlined />}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ type appSettings = {
|
|||||||
search_keyboard_layout?: "t9" | "qwerty26"
|
search_keyboard_layout?: "t9" | "qwerty26"
|
||||||
disable_search_keyboard?: boolean
|
disable_search_keyboard?: boolean
|
||||||
font_family?: string
|
font_family?: string
|
||||||
|
lan_access_enabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FontOption {
|
interface FontOption {
|
||||||
@@ -191,6 +192,7 @@ export const Settings: React.FC<{
|
|||||||
const [appQuitLoading, setAppQuitLoading] = useState(false)
|
const [appQuitLoading, setAppQuitLoading] = useState(false)
|
||||||
const [appRestartLoading, setAppRestartLoading] = useState(false)
|
const [appRestartLoading, setAppRestartLoading] = useState(false)
|
||||||
const [mcpLoading, setMcpLoading] = useState(false)
|
const [mcpLoading, setMcpLoading] = useState(false)
|
||||||
|
const [lanAccessLoading, setLanAccessLoading] = useState(false)
|
||||||
const [mcpConfig, setMcpConfig] = useState<{ host: string; port: number }>({
|
const [mcpConfig, setMcpConfig] = useState<{ host: string; port: number }>({
|
||||||
host: "127.0.0.1",
|
host: "127.0.0.1",
|
||||||
port: 3901,
|
port: 3901,
|
||||||
@@ -947,6 +949,47 @@ export const Settings: React.FC<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggleLanAccess = async (checked: boolean) => {
|
||||||
|
const api = (window as any).api
|
||||||
|
if (!api?.setSetting) return
|
||||||
|
setLanAccessLoading(true)
|
||||||
|
try {
|
||||||
|
if (checked) {
|
||||||
|
const statusRes = api.httpServerStatus ? await api.httpServerStatus() : null
|
||||||
|
const isRunning = Boolean(statusRes?.data?.is_running ?? statusRes?.data?.isRunning)
|
||||||
|
if (!isRunning) {
|
||||||
|
const startRes = await withTimeout(
|
||||||
|
api.httpServerStart?.({ host: "0.0.0.0", port: 45739, api_port: 45740 }),
|
||||||
|
10_000,
|
||||||
|
"启动局域网访问超时"
|
||||||
|
)
|
||||||
|
if (!startRes?.success) {
|
||||||
|
messageApi.error(startRes?.message || "启动局域网访问失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (api.httpServerStop) {
|
||||||
|
const stopRes = await withTimeout(api.httpServerStop(), 10_000, "停止局域网访问超时")
|
||||||
|
if (!stopRes?.success) {
|
||||||
|
messageApi.error(stopRes?.message || "停止局域网访问失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveRes = await api.setSetting("lan_access_enabled", checked)
|
||||||
|
if (saveRes?.success) {
|
||||||
|
setSettings((prev) => ({ ...prev, lan_access_enabled: checked }))
|
||||||
|
messageApi.success(t("settings.general.saved"))
|
||||||
|
} else {
|
||||||
|
messageApi.error(saveRes?.message || t("settings.general.saveFailed"))
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
messageApi.error(e?.message || "局域网访问设置失败")
|
||||||
|
} finally {
|
||||||
|
setLanAccessLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const currentYear = new Date().getFullYear()
|
const currentYear = new Date().getFullYear()
|
||||||
|
|
||||||
const confirmQuitApp = () => {
|
const confirmQuitApp = () => {
|
||||||
@@ -1938,6 +1981,23 @@ export const Settings: React.FC<{
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Divider />
|
<Divider />
|
||||||
|
<div style={{ fontSize: "16px", fontWeight: 600, marginBottom: "8px" }}>局域网访问</div>
|
||||||
|
<div
|
||||||
|
style={{ color: "var(--ss-text-secondary)", marginBottom: "12px", fontSize: "12px" }}
|
||||||
|
>
|
||||||
|
启用后会开放 45739 前端页面和 45740 API 服务,访问链接由主界面顶部链接按钮生成。
|
||||||
|
</div>
|
||||||
|
<Form layout="horizontal" labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
|
||||||
|
<Form.Item label="启用">
|
||||||
|
<Switch
|
||||||
|
checked={Boolean(settings.lan_access_enabled)}
|
||||||
|
loading={lanAccessLoading}
|
||||||
|
disabled={!canAdmin}
|
||||||
|
onChange={toggleLanAccess}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
<Divider />
|
||||||
<div style={{ fontSize: "16px", fontWeight: 600, marginBottom: "8px" }}>
|
<div style={{ fontSize: "16px", fontWeight: 600, marginBottom: "8px" }}>
|
||||||
{t("settings.mcp.title")}
|
{t("settings.mcp.title")}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+10
-1
@@ -8,8 +8,17 @@ import { ClientContext } from "./ClientContext"
|
|||||||
import { StudentService } from "./services/StudentService"
|
import { StudentService } from "./services/StudentService"
|
||||||
import { ServiceProvider } from "./contexts/ServiceContext"
|
import { ServiceProvider } from "./contexts/ServiceContext"
|
||||||
import { api } from "./preload/types"
|
import { api } from "./preload/types"
|
||||||
|
import { lanApi } from "./services/lanApi"
|
||||||
|
|
||||||
if (!(window as any).api) {
|
const hasTauriInvoke =
|
||||||
|
typeof (window as any).__TAURI_INTERNALS__?.invoke === "function" ||
|
||||||
|
typeof (window as any).__TAURI__?.core?.invoke === "function"
|
||||||
|
|
||||||
|
if (!hasTauriInvoke) {
|
||||||
|
;(window as any).__SECSCORE_LAN__ = true
|
||||||
|
;(window as any).api = lanApi
|
||||||
|
} else if (!(window as any).api) {
|
||||||
|
;(window as any).__SECSCORE_LAN__ = false
|
||||||
;(window as any).api = api
|
;(window as any).api = api
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+44
-3
@@ -96,6 +96,7 @@ export type settingsKey =
|
|||||||
| "pg_connection_string"
|
| "pg_connection_string"
|
||||||
| "pg_connection_status"
|
| "pg_connection_status"
|
||||||
| "mobile_bottom_nav_items"
|
| "mobile_bottom_nav_items"
|
||||||
|
| "lan_access_enabled"
|
||||||
|
|
||||||
export interface settingsSpec {
|
export interface settingsSpec {
|
||||||
is_wizard_completed: boolean
|
is_wizard_completed: boolean
|
||||||
@@ -117,6 +118,7 @@ export interface settingsSpec {
|
|||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
mobile_bottom_nav_items: string[]
|
mobile_bottom_nav_items: string[]
|
||||||
|
lan_access_enabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface pluginRuntimeModule {
|
export interface pluginRuntimeModule {
|
||||||
@@ -130,6 +132,13 @@ export interface pluginRuntimeModule {
|
|||||||
permissions: string[]
|
permissions: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface httpServerShareUrl {
|
||||||
|
ip: string
|
||||||
|
url: string
|
||||||
|
is_private?: boolean
|
||||||
|
is_192_168?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
// Theme
|
// Theme
|
||||||
getThemes: (): Promise<{ success: boolean; data: themeConfig[] }> => invoke("theme_list"),
|
getThemes: (): Promise<{ success: boolean; data: themeConfig[] }> => invoke("theme_list"),
|
||||||
@@ -723,13 +732,45 @@ const api = {
|
|||||||
httpServerStart: (config?: {
|
httpServerStart: (config?: {
|
||||||
port?: number
|
port?: number
|
||||||
host?: string
|
host?: string
|
||||||
|
api_port?: number
|
||||||
corsOrigin?: string
|
corsOrigin?: string
|
||||||
}): Promise<{ success: boolean; data: { url: string; config: any } }> =>
|
}): Promise<{
|
||||||
invoke("http_server_start", { config }),
|
success: boolean
|
||||||
|
data: {
|
||||||
|
url: string
|
||||||
|
api_url?: string
|
||||||
|
share_url?: string
|
||||||
|
share_urls?: httpServerShareUrl[]
|
||||||
|
token?: string
|
||||||
|
config: any
|
||||||
|
}
|
||||||
|
message?: string
|
||||||
|
}> => invoke("http_server_start", { config }),
|
||||||
|
httpServerRefreshToken: (): Promise<{
|
||||||
|
success: boolean
|
||||||
|
data?: {
|
||||||
|
url: string
|
||||||
|
api_url?: string
|
||||||
|
share_url?: string
|
||||||
|
share_urls?: httpServerShareUrl[]
|
||||||
|
token?: string
|
||||||
|
config: any
|
||||||
|
}
|
||||||
|
message?: string
|
||||||
|
}> => invoke("http_server_refresh_token"),
|
||||||
httpServerStop: (): Promise<{ success: boolean }> => invoke("http_server_stop"),
|
httpServerStop: (): Promise<{ success: boolean }> => invoke("http_server_stop"),
|
||||||
httpServerStatus: (): Promise<{
|
httpServerStatus: (): Promise<{
|
||||||
success: boolean
|
success: boolean
|
||||||
data: { isRunning: boolean; config?: any; url?: string }
|
data: {
|
||||||
|
is_running?: boolean
|
||||||
|
isRunning?: boolean
|
||||||
|
config?: any
|
||||||
|
url?: string
|
||||||
|
api_url?: string
|
||||||
|
share_url?: string
|
||||||
|
share_urls?: httpServerShareUrl[]
|
||||||
|
token?: string
|
||||||
|
}
|
||||||
}> => invoke("http_server_status"),
|
}> => invoke("http_server_status"),
|
||||||
|
|
||||||
// MCP Server
|
// MCP Server
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
const buildLanApiBase = () => {
|
||||||
|
const { protocol, hostname } = window.location
|
||||||
|
const apiProtocol = protocol === "https:" ? "https:" : "http:"
|
||||||
|
return `${apiProtocol}//${hostname}:45740`
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = async <T>(path: string, init?: RequestInit): Promise<T> => {
|
||||||
|
const res = await fetch(`${buildLanApiBase()}${path}`, {
|
||||||
|
...init,
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(init?.headers || {}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message:
|
||||||
|
res.status === 401 ? "局域网访问链接已失效,请重新扫码或复制最新链接" : res.statusText,
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
return (await res.json()) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultTheme = {
|
||||||
|
id: "light-default",
|
||||||
|
name: "Default",
|
||||||
|
mode: "light" as const,
|
||||||
|
config: {
|
||||||
|
tdesign: {
|
||||||
|
brandColor: "#1677ff",
|
||||||
|
warningColor: "#faad14",
|
||||||
|
errorColor: "#ff4d4f",
|
||||||
|
successColor: "#52c41a",
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
"--ss-bg-color": "#f5f7fb",
|
||||||
|
"--ss-header-bg": "#ffffff",
|
||||||
|
"--ss-card-bg": "#ffffff",
|
||||||
|
"--ss-border-color": "#e7e7e7",
|
||||||
|
"--ss-text-main": "#1f2329",
|
||||||
|
"--ss-text-secondary": "#6b7280",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const noopUnlisten = async () => () => void 0
|
||||||
|
|
||||||
|
const lanApiBase = {
|
||||||
|
getThemes: async () => ({ success: true, data: [defaultTheme] }),
|
||||||
|
getCurrentTheme: async () => ({ success: true, data: defaultTheme }),
|
||||||
|
setTheme: async () => ({ success: true }),
|
||||||
|
saveTheme: async () => ({ success: false, message: "LAN 模式不支持主题管理" }),
|
||||||
|
deleteTheme: async () => ({ success: false, message: "LAN 模式不支持主题管理" }),
|
||||||
|
onThemeChanged: noopUnlisten,
|
||||||
|
|
||||||
|
queryStudents: async () =>
|
||||||
|
request<{ success: boolean; data: any[]; message?: string }>("/api/students"),
|
||||||
|
queryReasons: async () =>
|
||||||
|
request<{ success: boolean; data: any[]; message?: string }>("/api/reasons"),
|
||||||
|
updateStudent: async () => ({ success: false, message: "LAN 模式不支持修改学生信息" }),
|
||||||
|
rewardSettingQuery: async () =>
|
||||||
|
request<{ success: boolean; data: any[]; message?: string }>("/api/rewards"),
|
||||||
|
queryEvents: async (params?: { limit?: number }) => {
|
||||||
|
const limit = Number(params?.limit || 100)
|
||||||
|
return request<{ success: boolean; data: any[]; message?: string }>(
|
||||||
|
`/api/events?limit=${limit}`
|
||||||
|
)
|
||||||
|
},
|
||||||
|
createEvent: async (data: any) =>
|
||||||
|
request<{ success: boolean; data?: number; message?: string }>("/api/events", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}),
|
||||||
|
deleteEvent: async (uuid: string) =>
|
||||||
|
request<{ success: boolean; message?: string }>(`/api/events/${encodeURIComponent(uuid)}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
}),
|
||||||
|
rewardRedeem: async () => ({ success: false, message: "LAN 模式仅支持加分和扣分" }),
|
||||||
|
|
||||||
|
getAllSettings: async () => ({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
is_wizard_completed: true,
|
||||||
|
log_level: "info",
|
||||||
|
window_zoom: 1,
|
||||||
|
search_keyboard_layout: "qwerty26",
|
||||||
|
disable_search_keyboard: false,
|
||||||
|
font_family: "system",
|
||||||
|
mobile_bottom_nav_items: ["home"],
|
||||||
|
lan_access_enabled: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
getSetting: async () => ({ success: false, message: "LAN 模式不支持设置" }),
|
||||||
|
setSetting: async () => ({ success: false, message: "LAN 模式不支持设置" }),
|
||||||
|
getSystemFonts: async () => ({ success: true, data: [] }),
|
||||||
|
|
||||||
|
dbGetStatus: async () => ({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
connected: true,
|
||||||
|
type: "sqlite",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
dbSync: async () => ({ success: true }),
|
||||||
|
dbSyncPreview: async () => ({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
can_sync: false,
|
||||||
|
need_sync: false,
|
||||||
|
local_only: 0,
|
||||||
|
remote_only: 0,
|
||||||
|
conflicts: [],
|
||||||
|
message: "LAN 模式不支持数据库同步",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
dbSyncApply: async () => ({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
success: false,
|
||||||
|
synced_records: 0,
|
||||||
|
resolved_conflicts: 0,
|
||||||
|
message: "LAN 模式不支持数据库同步",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
authGetStatus: async () => ({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
permission: "points",
|
||||||
|
hasAdminPassword: false,
|
||||||
|
hasPointsPassword: false,
|
||||||
|
hasRecoveryString: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
authLogin: async () => ({ success: true, data: { permission: "points" } }),
|
||||||
|
authLogout: async () => ({ success: true, data: { permission: "points" } }),
|
||||||
|
authSetPasswords: async () => ({ success: false, message: "LAN 模式不支持权限设置" }),
|
||||||
|
authGenerateRecovery: async () => ({ success: false, message: "LAN 模式不支持权限设置" }),
|
||||||
|
authResetByRecovery: async () => ({ success: false, message: "LAN 模式不支持权限设置" }),
|
||||||
|
authClearAll: async () => ({ success: false, message: "LAN 模式不支持权限设置" }),
|
||||||
|
|
||||||
|
oauthLoadLoginState: async () => ({ success: true, data: null }),
|
||||||
|
oauthClearLoginState: async () => ({ success: true }),
|
||||||
|
oauthGetStorageUsage: async () => ({ success: false, message: "LAN 模式不支持云空间查询" }),
|
||||||
|
onDeepLink: noopUnlisten,
|
||||||
|
onNavigate: noopUnlisten,
|
||||||
|
onSettingChanged: noopUnlisten,
|
||||||
|
onDataUpdated: noopUnlisten,
|
||||||
|
writeLog: async () => ({ success: true }),
|
||||||
|
|
||||||
|
openManagementWindow: async () => void 0,
|
||||||
|
windowMinimize: async () => void 0,
|
||||||
|
windowMaximize: async () => false,
|
||||||
|
windowClose: async () => void 0,
|
||||||
|
windowIsMaximized: async () => false,
|
||||||
|
onWindowMaximizedChanged: noopUnlisten,
|
||||||
|
windowResize: async () => void 0,
|
||||||
|
windowSetResizable: async () => void 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const lanApi = new Proxy(lanApiBase, {
|
||||||
|
get(target, prop, receiver) {
|
||||||
|
if (prop in target) return Reflect.get(target, prop, receiver)
|
||||||
|
if (typeof prop !== "string") return undefined
|
||||||
|
if (prop.startsWith("on")) return noopUnlisten
|
||||||
|
return async () => ({ success: false, message: `LAN 模式不支持 ${prop}` })
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user