From 47644695cf01d69fe15e7793016698903478f3cb Mon Sep 17 00:00:00 2001 From: "Gerry.tan" Date: Tue, 10 Feb 2026 10:50:21 +0800 Subject: [PATCH] feat: Support Web Remote Application Client Connection --- i18n/locales/en.json | 2 + i18n/locales/zh.json | 2 + public/icons/browser.png | Bin 0 -> 3607 bytes src-tauri/src/commands/get_connect_methods.rs | 38 ++++++ src-tauri/src/commands/mod.rs | 3 +- src-tauri/src/lib.rs | 2 + src-tauri/src/service/asset.rs | 6 +- src-tauri/src/service/connect_methods.rs | 20 ++++ src-tauri/src/service/mod.rs | 1 + ui/components/BasePage/basePage.vue | 1 + ui/components/Card/AssetIcon/assetIcon.vue | 3 +- .../ConnectionEditor/connectionEditor.vue | 4 + ui/components/EditForm/editForm.vue | 47 ++++++++ ui/components/SideBar/sideBar.vue | 6 + ui/composables/useAssetAction.ts | 29 ++++- ui/composables/useAssetConnection.ts | 5 +- ui/composables/useAssetFetcher.ts | 2 + ui/composables/useConnectMethods.ts | 111 ++++++++++++++++++ ui/pages/web.vue | 5 + ui/store/modules/userInfo.ts | 11 ++ ui/types/index.ts | 12 +- 21 files changed, 299 insertions(+), 11 deletions(-) create mode 100644 public/icons/browser.png create mode 100644 src-tauri/src/commands/get_connect_methods.rs create mode 100644 src-tauri/src/service/connect_methods.rs create mode 100644 ui/composables/useConnectMethods.ts create mode 100644 ui/pages/web.vue diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 59c8281d..b6651f4b 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -58,6 +58,7 @@ "DirectoryService": "Directory Service", "Resource": "Resource", "Database": "Database", + "Web": "Web", "OfflinePlayer": "Offline Player", "Favorite": "Favorite" }, @@ -94,6 +95,7 @@ "ModifyConnectionInfo": "Connect", "Description": "Next connections will use these settings by default, right-click to edit", "OptionalProtocol": "Protocol", + "ConnectMethod": "Connect Method", "OptionalAccount": "Account" }, "Layout": { diff --git a/i18n/locales/zh.json b/i18n/locales/zh.json index 21cb2a08..c15c300e 100644 --- a/i18n/locales/zh.json +++ b/i18n/locales/zh.json @@ -56,6 +56,7 @@ "DirectoryService": "目录服务", "Database": "数据库", "Device": "设备", + "Web": "Web", "Other": "其他", "Favorite": "收藏", "OfflinePlayer": "离线播放器", @@ -94,6 +95,7 @@ "ModifyConnectionInfo": "连接", "Description": "保存后,下次连接将默认使用这些设置,右击重新设置", "OptionalProtocol": "协议", + "ConnectMethod": "连接方法", "OptionalAccount": "账号" }, "SettingModal": { diff --git a/public/icons/browser.png b/public/icons/browser.png new file mode 100644 index 0000000000000000000000000000000000000000..98fcf3ec490d44fea22ced1a66345040fd47affe GIT binary patch literal 3607 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 dNkl b, + Err(e) => { + let _ = app.emit( + "get-connect-methods-failure", + json!({ "status": 401, "error": e.to_string() }), + ); + return; + } + }; + let connect_methods_service = ConnectMethodsService::new(site, bearer); + let connect_methods_data = connect_methods_service.get_connect_methods().await; + + if !connect_methods_data.success { + error!("获取 ConnectMethods 数据失败"); + + let _ = app.emit( + "get-connect-methods-failure", + json!({ "status": connect_methods_data.status }), + ); + return; + } + + info!("获取 ConnectMethods 数据成功"); + + let _ = app.emit( + "get-connect-methods-success", + json!({ "status": connect_methods_data.status, "data": connect_methods_data.data }), + ); +} \ No newline at end of file diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 17d18f77..bb7c126d 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -2,9 +2,11 @@ pub mod auth_login; pub mod get_asset_detail; pub mod get_assets; pub mod get_config; +pub mod get_connect_methods; pub mod get_setting; pub mod get_token; pub mod get_version_message; +pub mod http_callback; pub mod list_system_fonts; pub mod logout; pub mod pull_up; @@ -14,4 +16,3 @@ pub mod set_favorite; pub mod unfavorite; pub mod update_config; pub mod window_controls; -pub mod http_callback; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e49fdb36..5987d0d0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -11,6 +11,7 @@ use crate::commands::auth_login::{auth_cancel, auth_login, handle_auth_callback, use crate::commands::get_asset_detail::get_asset_detail; use crate::commands::get_assets::get_assets; use crate::commands::get_config::get_config; +use crate::commands::get_connect_methods::get_connect_methods; use crate::commands::get_setting::get_setting; use crate::commands::get_token::get_connect_token; use crate::commands::get_version_message::get_version_message; @@ -195,6 +196,7 @@ pub fn run() { toggle_maximize_window, update_config_selection, init_http_callback_server, + get_connect_methods, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/service/asset.rs b/src-tauri/src/service/asset.rs index 1b8bed3a..ecc17d4f 100644 --- a/src-tauri/src/service/asset.rs +++ b/src-tauri/src/service/asset.rs @@ -13,6 +13,7 @@ pub enum Category { WindowsAd, Database, Device, + Web, } #[derive(Serialize, Deserialize, Default, Debug, Clone)] @@ -43,7 +44,9 @@ impl AssetQuery { pub fn new(asset_type: Category, org: String) -> Self { let (r#type, category) = match asset_type { Category::Database | Category::Device => (None, Some(asset_type)), - Category::Linux | Category::Windows | Category::WindowsAd => (Some(asset_type), None), + Category::Linux | Category::Windows | Category::WindowsAd | Category::Web => { + (Some(asset_type), None) + } }; Self { @@ -115,6 +118,7 @@ impl AssetService { Category::WindowsAd => (Some(Category::WindowsAd), None), Category::Database => (None, Some(Category::Database)), Category::Device => (None, Some(Category::Device)), + Category::Web => (None, Some(Category::Web)), } }; diff --git a/src-tauri/src/service/connect_methods.rs b/src-tauri/src/service/connect_methods.rs new file mode 100644 index 00000000..cd2b9dbe --- /dev/null +++ b/src-tauri/src/service/connect_methods.rs @@ -0,0 +1,20 @@ +use crate::commands::requests::{get_with_response, ApiResponse}; + +pub struct ConnectMethodsService { + origin: String, + bearer_token: String, +} + +impl ConnectMethodsService { + pub fn new(origin: String, bearer_token: String) -> Self { + Self { + origin, + bearer_token, + } + } + + pub async fn get_connect_methods(&self) -> ApiResponse { + let url = format!("{}/api/v1/terminal/components/connect-methods/", self.origin); + get_with_response(&url, &self.bearer_token).await + } +} \ No newline at end of file diff --git a/src-tauri/src/service/mod.rs b/src-tauri/src/service/mod.rs index 61d23ac5..dfc002bf 100644 --- a/src-tauri/src/service/mod.rs +++ b/src-tauri/src/service/mod.rs @@ -9,3 +9,4 @@ pub(crate) mod token_oauth; pub(crate) mod user; pub(crate) mod http_callback; pub(crate) mod version; +pub(crate) mod connect_methods; diff --git a/ui/components/BasePage/basePage.vue b/ui/components/BasePage/basePage.vue index 5cd3375b..9f2a728d 100644 --- a/ui/components/BasePage/basePage.vue +++ b/ui/components/BasePage/basePage.vue @@ -163,6 +163,7 @@ const handleConnectAsset = async (asset: AssetItem) => { manualPassword: saved!.manualPassword || "", dynamicPassword: saved!.dynamicPassword || "", rememberSecret: !!saved!.rememberSecret, + connectMethod: saved!.connectMethod || "", availableProtocols: saved!.availableProtocols || [] }); } diff --git a/ui/components/Card/AssetIcon/assetIcon.vue b/ui/components/Card/AssetIcon/assetIcon.vue index c52d8d1d..acd2526d 100644 --- a/ui/components/Card/AssetIcon/assetIcon.vue +++ b/ui/components/Card/AssetIcon/assetIcon.vue @@ -25,7 +25,8 @@ const imageProps = computed(() => { mongodb: "/icons/mongodb.png", dameng: "/icons/dameng.png", clickhouse: "/icons/clickhouse.png", - windows_ad: "/icons/windows.png" + windows_ad: "/icons/windows.png", + website: "/icons/browser.png" }; const src = iconMap[props.type] || ""; diff --git a/ui/components/ConnectionEditor/connectionEditor.vue b/ui/components/ConnectionEditor/connectionEditor.vue index 175e44e2..4f607237 100644 --- a/ui/components/ConnectionEditor/connectionEditor.vue +++ b/ui/components/ConnectionEditor/connectionEditor.vue @@ -14,6 +14,7 @@ const draftManualUsername = ref(""); const draftManualPassword = ref(""); const draftDynamicPassword = ref(""); const draftRememberSecret = ref(false); +const draftConnectMethod = ref(""); let pendingResolve: ((info: any) => void) | null = null; let pendingReject: ((reason?: any) => void) | null = null; @@ -63,6 +64,7 @@ const initDraft = (asset: AssetItem) => { draftManualPassword.value = saved?.manualPassword || ""; draftDynamicPassword.value = saved?.dynamicPassword || ""; draftRememberSecret.value = saved?.rememberSecret || false; + draftConnectMethod.value = saved?.connectMethod || ""; }; /** @@ -109,6 +111,7 @@ const buildConnectionInfo = () => { manualPassword: draftManualPassword.value || "", dynamicPassword: draftDynamicPassword.value || "", rememberSecret: !!draftRememberSecret.value, + connectMethod: draftConnectMethod.value || "", availableProtocols: normalizeProtocols() }; }; @@ -208,6 +211,7 @@ defineExpose({ open: openModal, close }); v-model:manual-password="draftManualPassword" v-model:dynamic-password="draftDynamicPassword" v-model:remember-secret="draftRememberSecret" + v-model:connect-method="draftConnectMethod" :accounts="currentAsset.permedAccounts || []" :protocols="currentAsset.permedProtocols || []" /> diff --git a/ui/components/EditForm/editForm.vue b/ui/components/EditForm/editForm.vue index 23e61d56..9c619dfb 100644 --- a/ui/components/EditForm/editForm.vue +++ b/ui/components/EditForm/editForm.vue @@ -1,6 +1,7 @@ + + diff --git a/ui/store/modules/userInfo.ts b/ui/store/modules/userInfo.ts index e04ef2a5..6116d841 100644 --- a/ui/store/modules/userInfo.ts +++ b/ui/store/modules/userInfo.ts @@ -1,4 +1,5 @@ import type { ConnectionInfo, PermOrgItem, RdpGraphics, UserData } from "~/types/index"; +import { useConnectMethods } from "~/composables/useConnectMethods"; export type SiteUserData = UserData & { language?: string @@ -62,6 +63,16 @@ export const useUserInfoStore = defineStore( // 初始化当前站点连接信息映射以及 RDP 客户端选项 currentConnectionInfoMap.value = next.connectionInfoMap || {}; currentRdpClientOption.value = next.rdpClientOption || {}; + + // 登录后获取连接方法 + const { fetchConnectMethods } = useConnectMethods(); + nextTick(async () => { + try { + await fetchConnectMethods(); + } catch (error) { + console.debug("Failed to fetch connect methods on login:", error); + } + }); }; /** diff --git a/ui/types/index.ts b/ui/types/index.ts index 03fd990f..9df791e9 100644 --- a/ui/types/index.ts +++ b/ui/types/index.ts @@ -8,7 +8,7 @@ export type LangType = "zh" | "en"; export type LanguagePreference = LangType | "system"; export type CharsetType = "default" | "utf8" | "gbk" | "gb2312" | "ios-8859-1"; export type ResolutionType = "auto" | "1024x768" | "1366x768" | "1600x900" | "1920x1080"; -export type AssetPageType = "linux" | "windows" | "windows_ad" | "database" | "device" | "favorite"; +export type AssetPageType = "linux" | "windows" | "windows_ad" | "database" | "device" | "web" | "favorite"; export interface ActionItem { key: string @@ -224,6 +224,16 @@ export interface ConnectionInfo { rememberSecret?: boolean dynamicPassword?: string availableProtocols?: string[] + connectMethod?: string +} + +export interface ConnectMethod { + name: string + display_name: string + protocols: string[] + type: string + is_default: boolean + is_internal: boolean } export interface RdpGraphics {