feat: 新增局域网浏览器访问

This commit is contained in:
JSR
2026-07-11 12:20:01 +08:00
parent 7971c6adbc
commit 5595062278
10 changed files with 1749 additions and 30 deletions
+123 -5
View File
@@ -814,14 +814,38 @@ dependencies = [
"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]]
name = "darling"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
dependencies = [
"darling_core",
"darling_macro",
"darling_core 0.23.0",
"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]]
@@ -837,13 +861,24 @@ dependencies = [
"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]]
name = "darling_macro"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
dependencies = [
"darling_core",
"darling_core 0.23.0",
"quote",
"syn 2.0.117",
]
@@ -880,6 +915,37 @@ dependencies = [
"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]]
name = "derive_more"
version = "0.99.20"
@@ -1532,6 +1598,17 @@ dependencies = [
"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]]
name = "gio"
version = "0.18.4"
@@ -2379,6 +2456,17 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "lock_api"
version = "0.4.14"
@@ -2582,6 +2670,35 @@ dependencies = [
"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]]
name = "new_debug_unreachable"
version = "1.0.6"
@@ -3868,7 +3985,7 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab9d95d7ed26ad8306352b0d5f05b593222b272790564589790d210aa15caa9e"
dependencies = [
"darling",
"darling 0.23.0",
"proc-macro2",
"quote",
"serde_json",
@@ -4222,6 +4339,7 @@ dependencies = [
"dirs",
"hex",
"libc",
"local-ip-address",
"once_cell",
"parking_lot",
"rand 0.8.5",
@@ -4437,7 +4555,7 @@ version = "3.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65"
dependencies = [
"darling",
"darling 0.23.0",
"proc-macro2",
"quote",
"syn 2.0.117",
+1
View File
@@ -41,6 +41,7 @@ parking_lot = { version = "0.12", features = ["send_guard"] }
dirs = "6.0.0"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
urlencoding = "2.1"
local-ip-address = "0.6.13"
[target.'cfg(windows)'.dependencies]
winreg = "0.52"
File diff suppressed because it is too large Load Diff
+36
View File
@@ -148,6 +148,7 @@ pub fn run() {
fs_file_exists,
fs_open_path,
http_server_start,
http_server_refresh_token,
http_server_stop,
http_server_status,
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_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(())
}
+19
View File
@@ -20,6 +20,7 @@ pub struct SettingsSpec {
pub pg_connection_string: String,
pub pg_connection_status: JsonValue,
pub mobile_bottom_nav_items: JsonValue,
pub lan_access_enabled: bool,
}
impl Default for SettingsSpec {
@@ -51,6 +52,7 @@ impl Default for SettingsSpec {
"reasons",
"settings"
]),
lan_access_enabled: false,
}
}
}
@@ -72,6 +74,7 @@ pub enum SettingsKey {
PgConnectionString,
PgConnectionStatus,
MobileBottomNavItems,
LanAccessEnabled,
}
impl SettingsKey {
@@ -92,6 +95,7 @@ impl SettingsKey {
SettingsKey::PgConnectionString => "pg_connection_string",
SettingsKey::PgConnectionStatus => "pg_connection_status",
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_status" => Some(SettingsKey::PgConnectionStatus),
"mobile_bottom_nav_items" => Some(SettingsKey::MobileBottomNavItems),
"lan_access_enabled" => Some(SettingsKey::LanAccessEnabled),
_ => 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
}
@@ -636,6 +651,10 @@ impl SettingsService {
"settings"
]),
},
lan_access_enabled: match self.get_value(SettingsKey::LanAccessEnabled) {
SettingsValue::Boolean(b) => b,
_ => false,
},
}
}
+296 -3
View File
@@ -1,10 +1,12 @@
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 {
MenuFoldOutlined,
MenuUnfoldOutlined,
LeftOutlined,
SettingOutlined,
LinkOutlined,
CopyOutlined,
} from "@ant-design/icons"
import { Routes, Route, Navigate, useLocation, useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next"
@@ -84,6 +86,13 @@ interface HeaderStorageUsage {
file_count: number
}
interface LanShareUrl {
ip: string
url: string
is_private?: boolean
is_192_168?: boolean
}
export function ContentArea({
permission,
oauthUserName,
@@ -107,12 +116,13 @@ export function ContentArea({
bottomInset = 0,
}: ContentAreaProps): React.JSX.Element {
const { t } = useTranslation()
const isLanBrowser = Boolean((window as any).__SECSCORE_LAN__)
const isMacOS =
typeof navigator !== "undefined" &&
/mac/i.test(navigator.userAgent) &&
!/iphone|ipad|ipod|android/i.test(navigator.userAgent)
// macOS 使用原生红绿灯,侧栏隐藏(沉浸/竖屏)时顶部左侧需为红绿灯留白
const macTrafficLightsInset = isMacOS && (isPortraitMode || immersiveMode)
const macTrafficLightsInset = !isLanBrowser && isMacOS && (isPortraitMode || immersiveMode)
const location = useLocation()
const navigate = useNavigate()
const isSubPage = location.pathname !== "/" && !location.pathname.startsWith("/home")
@@ -200,7 +210,21 @@ export function ContentArea({
})
const [copiedUserId, setCopiedUserId] = 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 lanCopyResetTimerRef = useRef<number | null>(null)
const hasOAuthSession = Boolean(oauthUserName && oauthUserName.trim())
const formattedLastSyncTime = (() => {
if (!lastSyncTime) return "暂无"
@@ -290,6 +314,10 @@ export function ContentArea({
window.clearTimeout(copyResetTimerRef.current)
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 = (
<div style={{ width: "260px", display: "flex", flexDirection: "column", gap: "10px" }}>
<div
@@ -408,6 +557,134 @@ export function ContentArea({
</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 (
<Layout
style={{
@@ -549,7 +826,23 @@ export function ContentArea({
}
>
<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
size="small"
icon={<SettingOutlined />}
+60
View File
@@ -42,6 +42,7 @@ type appSettings = {
search_keyboard_layout?: "t9" | "qwerty26"
disable_search_keyboard?: boolean
font_family?: string
lan_access_enabled?: boolean
}
interface FontOption {
@@ -191,6 +192,7 @@ export const Settings: React.FC<{
const [appQuitLoading, setAppQuitLoading] = useState(false)
const [appRestartLoading, setAppRestartLoading] = useState(false)
const [mcpLoading, setMcpLoading] = useState(false)
const [lanAccessLoading, setLanAccessLoading] = useState(false)
const [mcpConfig, setMcpConfig] = useState<{ host: string; port: number }>({
host: "127.0.0.1",
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 confirmQuitApp = () => {
@@ -1938,6 +1981,23 @@ export const Settings: React.FC<{
</>
)}
<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" }}>
{t("settings.mcp.title")}
</div>
+10 -1
View File
@@ -8,8 +8,17 @@ import { ClientContext } from "./ClientContext"
import { StudentService } from "./services/StudentService"
import { ServiceProvider } from "./contexts/ServiceContext"
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
}
+44 -3
View File
@@ -96,6 +96,7 @@ export type settingsKey =
| "pg_connection_string"
| "pg_connection_status"
| "mobile_bottom_nav_items"
| "lan_access_enabled"
export interface settingsSpec {
is_wizard_completed: boolean
@@ -117,6 +118,7 @@ export interface settingsSpec {
error?: string
}
mobile_bottom_nav_items: string[]
lan_access_enabled: boolean
}
export interface pluginRuntimeModule {
@@ -130,6 +132,13 @@ export interface pluginRuntimeModule {
permissions: string[]
}
export interface httpServerShareUrl {
ip: string
url: string
is_private?: boolean
is_192_168?: boolean
}
const api = {
// Theme
getThemes: (): Promise<{ success: boolean; data: themeConfig[] }> => invoke("theme_list"),
@@ -723,13 +732,45 @@ const api = {
httpServerStart: (config?: {
port?: number
host?: string
api_port?: number
corsOrigin?: string
}): Promise<{ success: boolean; data: { url: string; config: any } }> =>
invoke("http_server_start", { config }),
}): Promise<{
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"),
httpServerStatus: (): Promise<{
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"),
// MCP Server
+170
View File
@@ -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}` })
},
})