chore(deeplink): integrate deep link handling into app lifecycle

Wire up deep link infrastructure with app initialization and event handling.

Backend Integration:
- Register deep link module and commands in mod.rs
- Add URL handling in app setup (src-tauri/src/lib.rs:handle_deeplink_url)
- Handle deep links from single instance callback (Windows/Linux CLI)
- Handle deep links from macOS system events
- Add tauri-plugin-deep-link dependency (Cargo.toml)

Frontend Integration:
- Listen for deeplink-import/deeplink-error events in App.tsx
- Update DeepLinkImportDialog component imports

Configuration:
- Enable deep link plugin in tauri.conf.json
- Update Cargo.lock for new dependencies

Localization:
- Add Chinese translations for deep link UI (zh.json)
- Add English translations for deep link UI (en.json)

Files: 9 changed, 359 insertions(+), 18 deletions(-)
This commit is contained in:
YoVinchen
2025-11-18 02:06:10 +08:00
parent 461ba6f418
commit 956e723781
9 changed files with 359 additions and 18 deletions

111
src-tauri/Cargo.lock generated
View File

@@ -579,6 +579,7 @@ dependencies = [
"serial_test", "serial_test",
"tauri", "tauri",
"tauri-build", "tauri-build",
"tauri-plugin-deep-link",
"tauri-plugin-dialog", "tauri-plugin-dialog",
"tauri-plugin-log", "tauri-plugin-log",
"tauri-plugin-opener", "tauri-plugin-opener",
@@ -591,6 +592,7 @@ dependencies = [
"tokio", "tokio",
"toml 0.8.2", "toml 0.8.2",
"toml_edit 0.22.27", "toml_edit 0.22.27",
"url",
] ]
[[package]] [[package]]
@@ -665,6 +667,26 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "const-random"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
dependencies = [
"const-random-macro",
]
[[package]]
name = "const-random-macro"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [
"getrandom 0.2.16",
"once_cell",
"tiny-keccak",
]
[[package]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.4.0" version = "0.4.0"
@@ -754,6 +776,12 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.6" version = "0.1.6"
@@ -983,6 +1011,15 @@ dependencies = [
"syn 2.0.106", "syn 2.0.106",
] ]
[[package]]
name = "dlv-list"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
dependencies = [
"const-random",
]
[[package]] [[package]]
name = "downcast-rs" name = "downcast-rs"
version = "1.2.1" version = "1.2.1"
@@ -1671,6 +1708,12 @@ dependencies = [
"ahash", "ahash",
] ]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.16.0" version = "0.16.0"
@@ -1747,6 +1790,12 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "http-range"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
[[package]] [[package]]
name = "httparse" name = "httparse"
version = "1.10.1" version = "1.10.1"
@@ -2778,6 +2827,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordered-multimap"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
dependencies = [
"dlv-list",
"hashbrown 0.14.5",
]
[[package]] [[package]]
name = "ordered-stream" name = "ordered-stream"
version = "0.2.0" version = "0.2.0"
@@ -3623,6 +3682,16 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "rust-ini"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
dependencies = [
"cfg-if",
"ordered-multimap",
]
[[package]] [[package]]
name = "rust_decimal" name = "rust_decimal"
version = "1.38.0" version = "1.38.0"
@@ -4362,6 +4431,7 @@ dependencies = [
"gtk", "gtk",
"heck 0.5.0", "heck 0.5.0",
"http", "http",
"http-range",
"jni", "jni",
"libc", "libc",
"log", "log",
@@ -4477,6 +4547,27 @@ dependencies = [
"walkdir", "walkdir",
] ]
[[package]]
name = "tauri-plugin-deep-link"
version = "2.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e82759f7c7d51de3cbde51c04b3f2332de52436ed84541182cd8944b04e9e73"
dependencies = [
"dunce",
"plist",
"rust-ini",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.17",
"tracing",
"url",
"windows-registry",
"windows-result 0.3.4",
]
[[package]] [[package]]
name = "tauri-plugin-dialog" name = "tauri-plugin-dialog"
version = "2.4.0" version = "2.4.0"
@@ -4831,6 +4922,15 @@ dependencies = [
"time-core", "time-core",
] ]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.1" version = "0.8.1"
@@ -5754,6 +5854,17 @@ dependencies = [
"windows-link 0.1.3", "windows-link 0.1.3",
] ]
[[package]]
name = "windows-registry"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
dependencies = [
"windows-link 0.1.3",
"windows-result 0.3.4",
"windows-strings 0.4.2",
]
[[package]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.3.4" version = "0.3.4"

View File

@@ -26,13 +26,14 @@ serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
log = "0.4" log = "0.4"
chrono = "0.4" chrono = "0.4"
tauri = { version = "2.8.2", features = ["tray-icon"] } tauri = { version = "2.8.2", features = ["tray-icon", "protocol-asset"] }
tauri-plugin-log = "2" tauri-plugin-log = "2"
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
tauri-plugin-process = "2" tauri-plugin-process = "2"
tauri-plugin-updater = "2" tauri-plugin-updater = "2"
tauri-plugin-dialog = "2" tauri-plugin-dialog = "2"
tauri-plugin-store = "2" tauri-plugin-store = "2"
tauri-plugin-deep-link = "2"
dirs = "5.0" dirs = "5.0"
toml = "0.8" toml = "0.8"
toml_edit = "0.22" toml_edit = "0.22"
@@ -42,6 +43,7 @@ futures = "0.3"
regex = "1.10" regex = "1.10"
rquickjs = { version = "0.8", features = ["array-buffer", "classes"] } rquickjs = { version = "0.8", features = ["array-buffer", "classes"] }
thiserror = "1.0" thiserror = "1.0"
url = "2.5"
[target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))'.dependencies] [target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))'.dependencies]
tauri-plugin-single-instance = "2" tauri-plugin-single-instance = "2"

View File

@@ -1,6 +1,7 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
mod config; mod config;
mod deeplink;
mod import_export; mod import_export;
mod mcp; mod mcp;
mod misc; mod misc;
@@ -10,6 +11,7 @@ mod provider;
mod settings; mod settings;
pub use config::*; pub use config::*;
pub use deeplink::*;
pub use import_export::*; pub use import_export::*;
pub use mcp::*; pub use mcp::*;
pub use misc::*; pub use misc::*;

View File

@@ -5,6 +5,7 @@ mod claude_plugin;
mod codex_config; mod codex_config;
mod commands; mod commands;
mod config; mod config;
mod deeplink;
mod error; mod error;
mod gemini_config; // 新增 mod gemini_config; // 新增
mod gemini_mcp; mod gemini_mcp;
@@ -22,6 +23,7 @@ pub use app_config::{AppType, McpApps, McpServer, MultiAppConfig};
pub use codex_config::{get_codex_auth_path, get_codex_config_path, write_codex_live_atomic}; pub use codex_config::{get_codex_auth_path, get_codex_config_path, write_codex_live_atomic};
pub use commands::*; pub use commands::*;
pub use config::{get_claude_mcp_path, get_claude_settings_path, read_json_file}; pub use config::{get_claude_mcp_path, get_claude_settings_path, read_json_file};
pub use deeplink::{import_provider_from_deeplink, parse_deeplink_url, DeepLinkImportRequest};
pub use error::AppError; pub use error::AppError;
pub use mcp::{ pub use mcp::{
import_from_claude, import_from_codex, import_from_gemini, remove_server_from_claude, import_from_claude, import_from_codex, import_from_gemini, remove_server_from_claude,
@@ -35,6 +37,7 @@ pub use services::{
}; };
pub use settings::{update_settings, AppSettings}; pub use settings::{update_settings, AppSettings};
pub use store::AppState; pub use store::AppState;
use tauri_plugin_deep_link::DeepLinkExt;
use tauri::{ use tauri::{
menu::{CheckMenuItem, Menu, MenuBuilder, MenuItem}, menu::{CheckMenuItem, Menu, MenuBuilder, MenuItem},
@@ -281,6 +284,65 @@ fn handle_tray_menu_event(app: &tauri::AppHandle, event_id: &str) {
} }
} }
/// 统一处理 ccswitch:// 深链接 URL
///
/// - 解析 URL
/// - 向前端发射 `deeplink-import` / `deeplink-error` 事件
/// - 可选:在成功时聚焦主窗口
fn handle_deeplink_url(
app: &tauri::AppHandle,
url_str: &str,
focus_main_window: bool,
source: &str,
) -> bool {
if !url_str.starts_with("ccswitch://") {
return false;
}
log::info!("✓ Deep link URL detected from {source}: {url_str}");
match crate::deeplink::parse_deeplink_url(url_str) {
Ok(request) => {
log::info!(
"✓ Successfully parsed deep link: resource={}, app={}, name={}",
request.resource,
request.app,
request.name
);
if let Err(e) = app.emit("deeplink-import", &request) {
log::error!("✗ Failed to emit deeplink-import event: {e}");
} else {
log::info!("✓ Emitted deeplink-import event to frontend");
}
if focus_main_window {
if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
log::info!("✓ Window shown and focused");
}
}
}
Err(e) => {
log::error!("✗ Failed to parse deep link URL: {e}");
if let Err(emit_err) = app.emit(
"deeplink-error",
serde_json::json!({
"url": url_str,
"error": e.to_string()
}),
) {
log::error!("✗ Failed to emit deeplink-error event: {emit_err}");
}
}
}
true
}
// //
/// 内部切换供应商函数 /// 内部切换供应商函数
@@ -346,7 +408,27 @@ pub fn run() {
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
{ {
builder = builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { builder = builder.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
log::info!("=== Single Instance Callback Triggered ===");
log::info!("Args count: {}", args.len());
for (i, arg) in args.iter().enumerate() {
log::info!(" arg[{i}]: {arg}");
}
// Check for deep link URL in args (mainly for Windows/Linux command line)
let mut found_deeplink = false;
for arg in &args {
if handle_deeplink_url(app, arg, false, "single_instance args") {
found_deeplink = true;
break;
}
}
if !found_deeplink {
log::info!(" No deep link URL found in args (this is expected on macOS when launched via system)");
}
// Show and focus window regardless
if let Some(window) = app.get_webview_window("main") { if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize(); let _ = window.unminimize();
let _ = window.show(); let _ = window.show();
@@ -356,6 +438,8 @@ pub fn run() {
} }
let builder = builder let builder = builder
// 注册 deep-link 插件(处理 macOS AppleEvent 和其他平台的深链接)
.plugin(tauri_plugin_deep_link::init())
// 拦截窗口关闭:根据设置决定是否最小化到托盘 // 拦截窗口关闭:根据设置决定是否最小化到托盘
.on_window_event(|window, event| { .on_window_event(|window, event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event { if let tauri::WindowEvent::CloseRequested { api, .. } = event {
@@ -471,7 +555,40 @@ pub fn run() {
config_guard.ensure_app(&app_config::AppType::Codex); config_guard.ensure_app(&app_config::AppType::Codex);
} }
// 启动阶段不再无条件保存避免意外覆盖用户配置。 // 启动阶段不再无条件保存,避免意外覆盖用户配置。
// 注册 deep-link URL 处理器(使用正确的 DeepLinkExt API
log::info!("=== Registering deep-link URL handler ===");
// Linux 和 Windows 调试模式需要显式注册
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
{
if let Err(e) = app.deep_link().register_all() {
log::error!("✗ Failed to register deep link schemes: {}", e);
} else {
log::info!("✓ Deep link schemes registered (Linux/Windows)");
}
}
// 注册 URL 处理回调(所有平台通用)
app.deep_link().on_open_url({
let app_handle = app.handle().clone();
move |event| {
log::info!("=== Deep Link Event Received (on_open_url) ===");
let urls = event.urls();
log::info!("Received {} URL(s)", urls.len());
for (i, url) in urls.iter().enumerate() {
let url_str = url.as_str();
log::info!(" URL[{i}]: {url_str}");
if handle_deeplink_url(&app_handle, url_str, true, "on_open_url") {
break; // Process only first ccswitch:// URL
}
}
}
});
log::info!("✓ Deep-link URL handler registered");
// 创建动态托盘菜单 // 创建动态托盘菜单
let menu = create_tray_menu(app.handle(), &app_state)?; let menu = create_tray_menu(app.handle(), &app_state)?;
@@ -572,6 +689,9 @@ pub fn run() {
commands::save_file_dialog, commands::save_file_dialog,
commands::open_file_dialog, commands::open_file_dialog,
commands::sync_current_providers_live, commands::sync_current_providers_live,
// Deep link import
commands::parse_deeplink,
commands::import_from_deeplink,
update_tray_menu, update_tray_menu,
]); ]);
@@ -581,17 +701,74 @@ pub fn run() {
app.run(|app_handle, event| { app.run(|app_handle, event| {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
// macOS 在 Dock 图标被点击并重新激活应用时会触发 Reopen 事件,这里手动恢复主窗口 {
if let RunEvent::Reopen { .. } = event { match event {
if let Some(window) = app_handle.get_webview_window("main") { // macOS 在 Dock 图标被点击并重新激活应用时会触发 Reopen 事件,这里手动恢复主窗口
#[cfg(target_os = "windows")] RunEvent::Reopen { .. } => {
{ if let Some(window) = app_handle.get_webview_window("main") {
let _ = window.set_skip_taskbar(false); #[cfg(target_os = "windows")]
{
let _ = window.set_skip_taskbar(false);
}
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
apply_tray_policy(app_handle, true);
}
} }
let _ = window.unminimize(); // 处理通过自定义 URL 协议触发的打开事件(例如 ccswitch://...
let _ = window.show(); RunEvent::Opened { urls } => {
let _ = window.set_focus(); if let Some(url) = urls.first() {
apply_tray_policy(app_handle, true); let url_str = url.to_string();
log::info!("RunEvent::Opened with URL: {url_str}");
if url_str.starts_with("ccswitch://") {
// 解析并广播深链接事件,复用与 single_instance 相同的逻辑
match crate::deeplink::parse_deeplink_url(&url_str) {
Ok(request) => {
log::info!(
"Successfully parsed deep link from RunEvent::Opened: resource={}, app={}",
request.resource,
request.app
);
if let Err(e) =
app_handle.emit("deeplink-import", &request)
{
log::error!(
"Failed to emit deep link event from RunEvent::Opened: {e}"
);
}
}
Err(e) => {
log::error!(
"Failed to parse deep link URL from RunEvent::Opened: {e}"
);
if let Err(emit_err) = app_handle.emit(
"deeplink-error",
serde_json::json!({
"url": url_str,
"error": e.to_string()
}),
) {
log::error!(
"Failed to emit deep link error event from RunEvent::Opened: {emit_err}"
);
}
}
}
// 确保主窗口可见
if let Some(window) = app_handle.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
}
}
}
}
_ => {}
} }
} }

View File

@@ -23,7 +23,11 @@
} }
], ],
"security": { "security": {
"csp": "default-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ipc: http://ipc.localhost https: http:" "csp": "default-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ipc: http://ipc.localhost https: http:",
"assetProtocol": {
"enable": true,
"scope": []
}
} }
}, },
"bundle": { "bundle": {
@@ -41,9 +45,17 @@
"wix": { "wix": {
"template": "wix/per-user-main.wxs" "template": "wix/per-user-main.wxs"
} }
},
"macOS": {
"minimumSystemVersion": "10.15"
} }
}, },
"plugins": { "plugins": {
"deep-link": {
"desktop": {
"schemes": ["ccswitch"]
}
},
"updater": { "updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEM4MDI4QzlBNTczOTI4RTMKUldUaktEbFhtb3dDeUM5US9kT0FmdGR5Ti9vQzcwa2dTMlpibDVDUmQ2M0VGTzVOWnd0SGpFVlEK", "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEM4MDI4QzlBNTczOTI4RTMKUldUaktEbFhtb3dDeUM5US9kT0FmdGR5Ti9vQzcwa2dTMlpibDVDUmQ2M0VGTzVOWnd0SGpFVlEK",
"endpoints": [ "endpoints": [

View File

@@ -22,6 +22,7 @@ import { UpdateBadge } from "@/components/UpdateBadge";
import UsageScriptModal from "@/components/UsageScriptModal"; import UsageScriptModal from "@/components/UsageScriptModal";
import UnifiedMcpPanel from "@/components/mcp/UnifiedMcpPanel"; import UnifiedMcpPanel from "@/components/mcp/UnifiedMcpPanel";
import PromptPanel from "@/components/prompts/PromptPanel"; import PromptPanel from "@/components/prompts/PromptPanel";
import { DeepLinkImportDialog } from "@/components/DeepLinkImportDialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
function App() { function App() {
@@ -303,6 +304,8 @@ function App() {
/> />
<UnifiedMcpPanel open={isMcpOpen} onOpenChange={setIsMcpOpen} /> <UnifiedMcpPanel open={isMcpOpen} onOpenChange={setIsMcpOpen} />
<DeepLinkImportDialog />
</div> </div>
); );
} }

View File

@@ -113,10 +113,8 @@ export function DeepLinkImportDialog() {
<div className="font-medium text-sm text-muted-foreground"> <div className="font-medium text-sm text-muted-foreground">
{t("deeplink.app")} {t("deeplink.app")}
</div> </div>
<div className="col-span-2 text-sm"> <div className="col-span-2 text-sm font-medium capitalize">
<span className="inline-flex items-center rounded-full bg-primary/10 px-2.5 py-0.5 text-xs font-medium text-primary capitalize"> {request.app}
{request.app}
</span>
</div> </div>
</div> </div>

View File

@@ -608,5 +608,23 @@
"deleteTitle": "Confirm Delete", "deleteTitle": "Confirm Delete",
"deleteMessage": "Are you sure you want to delete prompt \"{{name}}\"?" "deleteMessage": "Are you sure you want to delete prompt \"{{name}}\"?"
} }
},
"deeplink": {
"confirmImport": "Confirm Import Provider",
"confirmImportDescription": "The following configuration will be imported from deep link into CC Switch",
"app": "App Type",
"providerName": "Provider Name",
"homepage": "Homepage",
"endpoint": "API Endpoint",
"apiKey": "API Key",
"model": "Model",
"notes": "Notes",
"import": "Import",
"importing": "Importing...",
"warning": "Please confirm the information above is correct before importing. You can edit or delete it later in the provider list.",
"parseError": "Failed to parse deep link",
"importSuccess": "Import successful",
"importSuccessDescription": "Provider \"{{name}}\" has been successfully imported",
"importError": "Failed to import"
} }
} }

View File

@@ -608,5 +608,23 @@
"deleteTitle": "确认删除", "deleteTitle": "确认删除",
"deleteMessage": "确定要删除提示词 \"{{name}}\" 吗?" "deleteMessage": "确定要删除提示词 \"{{name}}\" 吗?"
} }
},
"deeplink": {
"confirmImport": "确认导入供应商配置",
"confirmImportDescription": "以下配置将导入到 CC Switch",
"app": "应用类型",
"providerName": "供应商名称",
"homepage": "官网地址",
"endpoint": "API 端点",
"apiKey": "API 密钥",
"model": "模型",
"notes": "备注",
"import": "导入",
"importing": "导入中...",
"warning": "请确认以上信息准确无误后再导入。导入后可在供应商列表中编辑或删除。",
"parseError": "深链接解析失败",
"importSuccess": "导入成功",
"importSuccessDescription": "供应商 \"{{name}}\" 已成功导入",
"importError": "导入失败"
} }
} }