配置导入导出
Some checks failed
Build Linux / Build Linux x86_64 (push) Has been cancelled
Build Test / Build Test (Linux) (push) Has been cancelled
Build Test / Build Test (Windows) (push) Has been cancelled
Build Test / Build Test (macOS) (push) Has been cancelled
Build Test / Build Test Summary (push) Has been cancelled
Some checks failed
Build Linux / Build Linux x86_64 (push) Has been cancelled
Build Test / Build Test (Linux) (push) Has been cancelled
Build Test / Build Test (Windows) (push) Has been cancelled
Build Test / Build Test (macOS) (push) Has been cancelled
Build Test / Build Test Summary (push) Has been cancelled
This commit is contained in:
8
bun.lock
8
bun.lock
@@ -11,6 +11,7 @@
|
|||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.1",
|
"@radix-ui/react-label": "^2.1.1",
|
||||||
"@radix-ui/react-popover": "^1.1.4",
|
"@radix-ui/react-popover": "^1.1.4",
|
||||||
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
"@radix-ui/react-radio-group": "^1.3.7",
|
"@radix-ui/react-radio-group": "^1.3.7",
|
||||||
"@radix-ui/react-select": "^2.1.3",
|
"@radix-ui/react-select": "^2.1.3",
|
||||||
"@radix-ui/react-switch": "^1.1.3",
|
"@radix-ui/react-switch": "^1.1.3",
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
"@tauri-apps/api": "^2.1.1",
|
"@tauri-apps/api": "^2.1.1",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
|
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.0.2",
|
"@tauri-apps/plugin-dialog": "^2.0.2",
|
||||||
|
"@tauri-apps/plugin-fs": "^2.4.2",
|
||||||
"@tauri-apps/plugin-global-shortcut": "^2.0.0",
|
"@tauri-apps/plugin-global-shortcut": "^2.0.0",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@tauri-apps/plugin-shell": "^2.0.1",
|
"@tauri-apps/plugin-shell": "^2.0.1",
|
||||||
@@ -345,6 +347,8 @@
|
|||||||
|
|
||||||
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="],
|
||||||
|
|
||||||
"@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.3.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9w5XhD0KPOrm92OTTE0SysH3sYzHsSTHNvZgUBo/VZ80VdYyB5RneDbc0dKpURS24IxkoFRu/hI0i4XyfFwY6g=="],
|
"@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.3.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9w5XhD0KPOrm92OTTE0SysH3sYzHsSTHNvZgUBo/VZ80VdYyB5RneDbc0dKpURS24IxkoFRu/hI0i4XyfFwY6g=="],
|
||||||
|
|
||||||
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q=="],
|
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q=="],
|
||||||
@@ -489,6 +493,8 @@
|
|||||||
|
|
||||||
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.2.2", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-Pm9qnXQq8ZVhAMFSEPwxvh+nWb2mk7LASVlNEHYaksHvcz8P6+ElR5U5dNL9Ofrm+uwhh1/gYKWswK8JJJAh6A=="],
|
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.2.2", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-Pm9qnXQq8ZVhAMFSEPwxvh+nWb2mk7LASVlNEHYaksHvcz8P6+ElR5U5dNL9Ofrm+uwhh1/gYKWswK8JJJAh6A=="],
|
||||||
|
|
||||||
|
"@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.4.2", "https://registry.npmmirror.com/@tauri-apps/plugin-fs/-/plugin-fs-2.4.2.tgz", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-YGhmYuTgXGsi6AjoV+5mh2NvicgWBfVJHHheuck6oHD+HC9bVWPaHvCP0/Aw4pHDejwrvT8hE3+zZAaWf+hrig=="],
|
||||||
|
|
||||||
"@tauri-apps/plugin-global-shortcut": ["@tauri-apps/plugin-global-shortcut@2.2.1", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-b64/TI1t5LIi2JY4OWlYjZpPRq60T5GVVL/no27sUuxaNUZY8dVtwsMtDUgxUpln2yR+P2PJsYlqY5V8sLSxEw=="],
|
"@tauri-apps/plugin-global-shortcut": ["@tauri-apps/plugin-global-shortcut@2.2.1", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-b64/TI1t5LIi2JY4OWlYjZpPRq60T5GVVL/no27sUuxaNUZY8dVtwsMtDUgxUpln2yR+P2PJsYlqY5V8sLSxEw=="],
|
||||||
|
|
||||||
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.3.0", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-yAbauwp8BCHIhhA48NN8rEf6OtfZBPCgTOCa10gmtoVCpmic5Bq+1Ba7C+NZOjogedkSiV7hAotjYnnbUVmYrw=="],
|
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.3.0", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-yAbauwp8BCHIhhA48NN8rEf6OtfZBPCgTOCa10gmtoVCpmic5Bq+1Ba7C+NZOjogedkSiV7hAotjYnnbUVmYrw=="],
|
||||||
@@ -1413,6 +1419,8 @@
|
|||||||
|
|
||||||
"@tauri-apps/plugin-clipboard-manager/@tauri-apps/api": ["@tauri-apps/api@2.7.0", "https://registry.npmmirror.com/@tauri-apps/api/-/api-2.7.0.tgz", {}, "sha512-v7fVE8jqBl8xJFOcBafDzXFc8FnicoH3j8o8DNNs0tHuEBmXUDqrCOAzMRX0UkfpwqZLqvrvK0GNQ45DfnoVDg=="],
|
"@tauri-apps/plugin-clipboard-manager/@tauri-apps/api": ["@tauri-apps/api@2.7.0", "https://registry.npmmirror.com/@tauri-apps/api/-/api-2.7.0.tgz", {}, "sha512-v7fVE8jqBl8xJFOcBafDzXFc8FnicoH3j8o8DNNs0tHuEBmXUDqrCOAzMRX0UkfpwqZLqvrvK0GNQ45DfnoVDg=="],
|
||||||
|
|
||||||
|
"@tauri-apps/plugin-fs/@tauri-apps/api": ["@tauri-apps/api@2.8.0", "https://registry.npmmirror.com/@tauri-apps/api/-/api-2.8.0.tgz", {}, "sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw=="],
|
||||||
|
|
||||||
"@types/estree-jsx/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
"@types/estree-jsx/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
@@ -21,6 +21,7 @@
|
|||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.1",
|
"@radix-ui/react-label": "^2.1.1",
|
||||||
"@radix-ui/react-popover": "^1.1.4",
|
"@radix-ui/react-popover": "^1.1.4",
|
||||||
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
"@radix-ui/react-radio-group": "^1.3.7",
|
"@radix-ui/react-radio-group": "^1.3.7",
|
||||||
"@radix-ui/react-select": "^2.1.3",
|
"@radix-ui/react-select": "^2.1.3",
|
||||||
"@radix-ui/react-switch": "^1.1.3",
|
"@radix-ui/react-switch": "^1.1.3",
|
||||||
@@ -33,6 +34,7 @@
|
|||||||
"@tauri-apps/api": "^2.1.1",
|
"@tauri-apps/api": "^2.1.1",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
|
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.0.2",
|
"@tauri-apps/plugin-dialog": "^2.0.2",
|
||||||
|
"@tauri-apps/plugin-fs": "^2.4.2",
|
||||||
"@tauri-apps/plugin-global-shortcut": "^2.0.0",
|
"@tauri-apps/plugin-global-shortcut": "^2.0.0",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@tauri-apps/plugin-shell": "^2.0.1",
|
"@tauri-apps/plugin-shell": "^2.0.1",
|
||||||
|
@@ -699,4 +699,212 @@ pub async fn relay_station_get_current_config() -> Result<HashMap<String, Option
|
|||||||
);
|
);
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 导出所有中转站配置
|
||||||
|
#[command]
|
||||||
|
pub async fn relay_stations_export(db: State<'_, AgentDb>) -> Result<Vec<RelayStation>, String> {
|
||||||
|
let conn = db.0.lock().map_err(|e| {
|
||||||
|
log::error!("Failed to acquire database lock: {}", e);
|
||||||
|
i18n::t("database.lock_failed")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 确保表存在
|
||||||
|
init_relay_stations_tables(&conn).map_err(|e| {
|
||||||
|
log::error!("Failed to initialize relay stations tables: {}", e);
|
||||||
|
i18n::t("database.init_failed")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut stmt = conn.prepare("SELECT * FROM relay_stations ORDER BY created_at DESC")
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("Failed to prepare statement: {}", e);
|
||||||
|
i18n::t("database.query_failed")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let stations = stmt.query_map([], |row| RelayStation::from_row(row))
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("Failed to query relay stations: {}", e);
|
||||||
|
i18n::t("database.query_failed")
|
||||||
|
})?
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("Failed to collect relay stations: {}", e);
|
||||||
|
i18n::t("database.query_failed")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
log::info!("Exported {} relay stations", stations.len());
|
||||||
|
Ok(stations)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 导入结果统计
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ImportResult {
|
||||||
|
pub total: usize, // 总数
|
||||||
|
pub imported: usize, // 成功导入数
|
||||||
|
pub skipped: usize, // 跳过数(重复)
|
||||||
|
pub failed: usize, // 失败数
|
||||||
|
pub message: String, // 结果消息
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 导入中转站配置
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ImportRelayStationsRequest {
|
||||||
|
pub stations: Vec<CreateRelayStationRequest>,
|
||||||
|
pub clear_existing: bool, // 是否清除现有配置
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn relay_stations_import(
|
||||||
|
request: ImportRelayStationsRequest,
|
||||||
|
db: State<'_, AgentDb>
|
||||||
|
) -> Result<ImportResult, String> {
|
||||||
|
let mut conn = db.0.lock().map_err(|e| {
|
||||||
|
log::error!("Failed to acquire database lock: {}", e);
|
||||||
|
i18n::t("database.lock_failed")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 确保表存在
|
||||||
|
init_relay_stations_tables(&conn).map_err(|e| {
|
||||||
|
log::error!("Failed to initialize relay stations tables: {}", e);
|
||||||
|
i18n::t("database.init_failed")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 开始事务
|
||||||
|
let tx = conn.transaction().map_err(|e| {
|
||||||
|
log::error!("Failed to start transaction: {}", e);
|
||||||
|
i18n::t("database.transaction_failed")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 如果需要清除现有配置
|
||||||
|
if request.clear_existing {
|
||||||
|
tx.execute("DELETE FROM relay_stations", [])
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("Failed to clear existing relay stations: {}", e);
|
||||||
|
i18n::t("relay_station.clear_failed")
|
||||||
|
})?;
|
||||||
|
log::info!("Cleared existing relay stations");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取现有的中转站列表(用于重复检查)
|
||||||
|
let existing_stations: Vec<(String, String)> = if !request.clear_existing {
|
||||||
|
let mut stmt = tx.prepare("SELECT api_url, system_token FROM relay_stations")
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("Failed to prepare statement: {}", e);
|
||||||
|
i18n::t("database.query_failed")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let stations_iter = stmt.query_map([], |row| {
|
||||||
|
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
|
||||||
|
})
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("Failed to query existing stations: {}", e);
|
||||||
|
i18n::t("database.query_failed")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 立即收集结果,避免生命周期问题
|
||||||
|
let mut existing = Vec::new();
|
||||||
|
for station_result in stations_iter {
|
||||||
|
match station_result {
|
||||||
|
Ok(station) => existing.push(station),
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to read existing station: {}", e);
|
||||||
|
return Err(i18n::t("database.query_failed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
existing
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导入新的中转站
|
||||||
|
let total = request.stations.len();
|
||||||
|
let mut imported_count = 0;
|
||||||
|
let mut skipped_count = 0;
|
||||||
|
let mut failed_count = 0;
|
||||||
|
let now = Utc::now().timestamp();
|
||||||
|
|
||||||
|
for station_request in request.stations {
|
||||||
|
// 验证输入
|
||||||
|
if let Err(e) = validate_relay_station_request(&station_request.name, &station_request.api_url, &station_request.system_token) {
|
||||||
|
log::warn!("Skipping invalid station {}: {}", station_request.name, e);
|
||||||
|
failed_count += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否重复(同时匹配 api_url 和 system_token)
|
||||||
|
let is_duplicate = existing_stations.iter().any(|(url, token)| {
|
||||||
|
url == &station_request.api_url && token == &station_request.system_token
|
||||||
|
});
|
||||||
|
|
||||||
|
if is_duplicate {
|
||||||
|
log::info!("Skipping duplicate station: {} ({})", station_request.name, station_request.api_url);
|
||||||
|
skipped_count += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
let adapter_str = serde_json::to_string(&station_request.adapter)
|
||||||
|
.map_err(|_| i18n::t("relay_station.invalid_adapter"))?
|
||||||
|
.trim_matches('"').to_string();
|
||||||
|
|
||||||
|
let auth_method_str = serde_json::to_string(&station_request.auth_method)
|
||||||
|
.map_err(|_| i18n::t("relay_station.invalid_auth_method"))?
|
||||||
|
.trim_matches('"').to_string();
|
||||||
|
|
||||||
|
let adapter_config_str = station_request.adapter_config.as_ref()
|
||||||
|
.map(|config| serde_json::to_string(config))
|
||||||
|
.transpose()
|
||||||
|
.map_err(|_| i18n::t("relay_station.invalid_config"))?;
|
||||||
|
|
||||||
|
match tx.execute(
|
||||||
|
r#"
|
||||||
|
INSERT INTO relay_stations
|
||||||
|
(id, name, description, api_url, adapter, auth_method, system_token, user_id, adapter_config, enabled, created_at, updated_at)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)
|
||||||
|
"#,
|
||||||
|
params![
|
||||||
|
id,
|
||||||
|
station_request.name,
|
||||||
|
station_request.description,
|
||||||
|
station_request.api_url,
|
||||||
|
adapter_str,
|
||||||
|
auth_method_str,
|
||||||
|
station_request.system_token,
|
||||||
|
station_request.user_id,
|
||||||
|
adapter_config_str,
|
||||||
|
if station_request.enabled { 1 } else { 0 },
|
||||||
|
now,
|
||||||
|
now
|
||||||
|
],
|
||||||
|
) {
|
||||||
|
Ok(_) => imported_count += 1,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to import relay station: {}", e);
|
||||||
|
failed_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交事务
|
||||||
|
tx.commit().map_err(|e| {
|
||||||
|
log::error!("Failed to commit transaction: {}", e);
|
||||||
|
i18n::t("database.transaction_failed")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let message = format!(
|
||||||
|
"导入完成:总计 {} 个,成功 {} 个,跳过 {} 个(重复),失败 {} 个",
|
||||||
|
total, imported_count, skipped_count, failed_count
|
||||||
|
);
|
||||||
|
|
||||||
|
log::info!("{}", message);
|
||||||
|
|
||||||
|
Ok(ImportResult {
|
||||||
|
total,
|
||||||
|
imported: imported_count,
|
||||||
|
skipped: skipped_count,
|
||||||
|
failed: failed_count,
|
||||||
|
message,
|
||||||
|
})
|
||||||
}
|
}
|
@@ -58,6 +58,7 @@ use commands::relay_stations::{
|
|||||||
relay_stations_list, relay_station_get, relay_station_create, relay_station_update,
|
relay_stations_list, relay_station_get, relay_station_create, relay_station_update,
|
||||||
relay_station_delete, relay_station_toggle_enable, relay_station_sync_config,
|
relay_station_delete, relay_station_toggle_enable, relay_station_sync_config,
|
||||||
relay_station_restore_config, relay_station_get_current_config,
|
relay_station_restore_config, relay_station_get_current_config,
|
||||||
|
relay_stations_export, relay_stations_import,
|
||||||
};
|
};
|
||||||
use commands::relay_adapters::{
|
use commands::relay_adapters::{
|
||||||
relay_station_get_info, relay_station_get_user_info,
|
relay_station_get_info, relay_station_get_user_info,
|
||||||
@@ -97,6 +98,7 @@ fn main() {
|
|||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
|
.plugin(tauri_plugin_fs::init())
|
||||||
.plugin(tauri_plugin_clipboard_manager::init())
|
.plugin(tauri_plugin_clipboard_manager::init())
|
||||||
.plugin(tauri_plugin_log::Builder::new()
|
.plugin(tauri_plugin_log::Builder::new()
|
||||||
.level(log::LevelFilter::Debug)
|
.level(log::LevelFilter::Debug)
|
||||||
@@ -382,6 +384,8 @@ fn main() {
|
|||||||
relay_station_sync_config,
|
relay_station_sync_config,
|
||||||
relay_station_restore_config,
|
relay_station_restore_config,
|
||||||
relay_station_get_current_config,
|
relay_station_get_current_config,
|
||||||
|
relay_stations_export,
|
||||||
|
relay_stations_import,
|
||||||
relay_station_get_info,
|
relay_station_get_info,
|
||||||
relay_station_get_user_info,
|
relay_station_get_user_info,
|
||||||
relay_station_test_connection,
|
relay_station_test_connection,
|
||||||
|
@@ -28,22 +28,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"fs": {
|
|
||||||
"scope": [
|
|
||||||
"$HOME/**"
|
|
||||||
],
|
|
||||||
"allow": [
|
|
||||||
"readFile",
|
|
||||||
"writeFile",
|
|
||||||
"readDir",
|
|
||||||
"copyFile",
|
|
||||||
"createDir",
|
|
||||||
"removeDir",
|
|
||||||
"removeFile",
|
|
||||||
"renameFile",
|
|
||||||
"exists"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"shell": {
|
"shell": {
|
||||||
"open": true
|
"open": true
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,8 @@ import MonacoEditor from '@monaco-editor/react';
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -27,6 +29,7 @@ import {
|
|||||||
RelayStationAdapter,
|
RelayStationAdapter,
|
||||||
AuthMethod,
|
AuthMethod,
|
||||||
PackycodeUserQuota,
|
PackycodeUserQuota,
|
||||||
|
ImportResult,
|
||||||
api
|
api
|
||||||
} from '@/lib/api';
|
} from '@/lib/api';
|
||||||
import {
|
import {
|
||||||
@@ -42,7 +45,9 @@ import {
|
|||||||
Eye,
|
Eye,
|
||||||
Edit3,
|
Edit3,
|
||||||
Save,
|
Save,
|
||||||
X
|
X,
|
||||||
|
Download,
|
||||||
|
Upload
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
interface RelayStationManagerProps {
|
interface RelayStationManagerProps {
|
||||||
@@ -66,6 +71,11 @@ const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) =>
|
|||||||
const [savingConfig, setSavingConfig] = useState(false);
|
const [savingConfig, setSavingConfig] = useState(false);
|
||||||
const [flushingDns, setFlushingDns] = useState(false);
|
const [flushingDns, setFlushingDns] = useState(false);
|
||||||
const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null);
|
const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null);
|
||||||
|
|
||||||
|
// 导入进度相关状态
|
||||||
|
const [importing, setImporting] = useState(false);
|
||||||
|
const [importProgress, setImportProgress] = useState(0);
|
||||||
|
const [importResult, setImportResult] = useState<ImportResult | null>(null);
|
||||||
|
|
||||||
// PackyCode 额度相关状态
|
// PackyCode 额度相关状态
|
||||||
const [quotaData, setQuotaData] = useState<Record<string, PackycodeUserQuota>>({});
|
const [quotaData, setQuotaData] = useState<Record<string, PackycodeUserQuota>>({});
|
||||||
@@ -199,6 +209,124 @@ const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) =>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 导出中转站配置
|
||||||
|
const handleExportStations = async () => {
|
||||||
|
try {
|
||||||
|
const stations = await api.relayStationsExport();
|
||||||
|
const jsonData = JSON.stringify(stations, null, 2);
|
||||||
|
|
||||||
|
// 使用 Tauri 的保存文件对话框
|
||||||
|
const { save } = await import('@tauri-apps/plugin-dialog');
|
||||||
|
const filePath = await save({
|
||||||
|
defaultPath: `relay-stations-${new Date().toISOString().slice(0, 10)}.json`,
|
||||||
|
filters: [{
|
||||||
|
name: 'JSON',
|
||||||
|
extensions: ['json']
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filePath) {
|
||||||
|
// 使用 Tauri 的文件系统 API 写入文件
|
||||||
|
const { writeTextFile } = await import('@tauri-apps/plugin-fs');
|
||||||
|
await writeTextFile(filePath, jsonData);
|
||||||
|
showToast(t('relayStation.exportSuccess'), 'success');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to export stations:', error);
|
||||||
|
showToast(t('relayStation.exportFailed'), 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导入中转站配置
|
||||||
|
const handleImportStations = async () => {
|
||||||
|
try {
|
||||||
|
setImporting(true);
|
||||||
|
setImportProgress(0);
|
||||||
|
setImportResult(null);
|
||||||
|
|
||||||
|
// 使用 Tauri 的文件选择对话框
|
||||||
|
const { open } = await import('@tauri-apps/plugin-dialog');
|
||||||
|
const selected = await open({
|
||||||
|
multiple: false,
|
||||||
|
filters: [{
|
||||||
|
name: 'JSON',
|
||||||
|
extensions: ['json']
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!selected) {
|
||||||
|
setImporting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setImportProgress(20);
|
||||||
|
|
||||||
|
// 使用 Tauri 的文件系统 API 读取文件
|
||||||
|
const { readTextFile } = await import('@tauri-apps/plugin-fs');
|
||||||
|
const text = await readTextFile(selected as string);
|
||||||
|
const stations = JSON.parse(text) as RelayStation[];
|
||||||
|
|
||||||
|
setImportProgress(40);
|
||||||
|
|
||||||
|
// 转换为 CreateRelayStationRequest 格式
|
||||||
|
const importRequests: CreateRelayStationRequest[] = stations.map(station => ({
|
||||||
|
name: station.name,
|
||||||
|
description: station.description,
|
||||||
|
api_url: station.api_url,
|
||||||
|
adapter: station.adapter,
|
||||||
|
auth_method: station.auth_method,
|
||||||
|
system_token: station.system_token,
|
||||||
|
user_id: station.user_id,
|
||||||
|
adapter_config: station.adapter_config,
|
||||||
|
enabled: station.enabled
|
||||||
|
}));
|
||||||
|
|
||||||
|
setImportProgress(60);
|
||||||
|
|
||||||
|
// 显示确认对话框
|
||||||
|
const confirmed = await new Promise<boolean>((resolve) => {
|
||||||
|
if (window.confirm(t('relayStation.importConfirm', { count: stations.length }))) {
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (confirmed) {
|
||||||
|
setImportProgress(80);
|
||||||
|
const result = await api.relayStationsImport(importRequests, false);
|
||||||
|
setImportProgress(100);
|
||||||
|
setImportResult(result);
|
||||||
|
|
||||||
|
// 显示结果
|
||||||
|
if (result.imported > 0) {
|
||||||
|
showToast(result.message, 'success');
|
||||||
|
loadStations();
|
||||||
|
} else if (result.skipped === result.total) {
|
||||||
|
showToast(t('relayStation.allDuplicate'), 'error');
|
||||||
|
} else {
|
||||||
|
showToast(result.message, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3秒后清除结果
|
||||||
|
setTimeout(() => {
|
||||||
|
setImportResult(null);
|
||||||
|
setImporting(false);
|
||||||
|
setImportProgress(0);
|
||||||
|
}, 3000);
|
||||||
|
} else {
|
||||||
|
setImporting(false);
|
||||||
|
setImportProgress(0);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to import stations:', error);
|
||||||
|
showToast(t('relayStation.importFailed'), 'error');
|
||||||
|
setImporting(false);
|
||||||
|
setImportProgress(0);
|
||||||
|
setImportResult(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 删除中转站
|
// 删除中转站
|
||||||
const deleteStation = async () => {
|
const deleteStation = async () => {
|
||||||
if (!stationToDelete) return;
|
if (!stationToDelete) return;
|
||||||
@@ -308,25 +436,85 @@ const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) =>
|
|||||||
<p className="text-muted-foreground">{t('relayStation.description')}</p>
|
<p className="text-muted-foreground">{t('relayStation.description')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
|
<div className="flex gap-2">
|
||||||
<DialogTrigger asChild>
|
<Button
|
||||||
<Button>
|
variant="outline"
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
onClick={handleExportStations}
|
||||||
{t('relayStation.create')}
|
>
|
||||||
</Button>
|
<Download className="mr-2 h-4 w-4" />
|
||||||
</DialogTrigger>
|
{t('relayStation.export')}
|
||||||
<DialogContent className="sm:max-w-[900px] max-h-[90vh] overflow-y-auto">
|
</Button>
|
||||||
<CreateStationDialog
|
<Button
|
||||||
onSuccess={() => {
|
variant="outline"
|
||||||
setShowCreateDialog(false);
|
onClick={handleImportStations}
|
||||||
loadStations();
|
>
|
||||||
showToast(t('relayStation.createSuccess'), "success");
|
<Upload className="mr-2 h-4 w-4" />
|
||||||
}}
|
{t('relayStation.import')}
|
||||||
/>
|
</Button>
|
||||||
</DialogContent>
|
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
|
||||||
</Dialog>
|
<DialogTrigger asChild>
|
||||||
|
<Button>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
{t('relayStation.create')}
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[900px] max-h-[90vh] overflow-y-auto">
|
||||||
|
<CreateStationDialog
|
||||||
|
onSuccess={() => {
|
||||||
|
setShowCreateDialog(false);
|
||||||
|
loadStations();
|
||||||
|
showToast(t('relayStation.createSuccess'), "success");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 导入进度 */}
|
||||||
|
{importing && (
|
||||||
|
<Card className="mb-6">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between mb-2">
|
||||||
|
<span className="text-sm font-medium">{t('relayStation.importing')}</span>
|
||||||
|
<span className="text-sm text-muted-foreground">{importProgress}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={importProgress} className="w-full" />
|
||||||
|
</div>
|
||||||
|
{importResult && (
|
||||||
|
<Alert>
|
||||||
|
<AlertDescription className="space-y-2">
|
||||||
|
<div className="font-medium">{importResult.message}</div>
|
||||||
|
<div className="text-sm space-y-1">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">{t('relayStation.importTotal')}:</span>
|
||||||
|
<span>{importResult.total}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">{t('relayStation.importSuccess')}:</span>
|
||||||
|
<span className="text-green-600">{importResult.imported}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">{t('relayStation.importSkipped')}:</span>
|
||||||
|
<span className="text-yellow-600">{importResult.skipped}</span>
|
||||||
|
</div>
|
||||||
|
{importResult.failed > 0 && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">{t('relayStation.importFailed')}:</span>
|
||||||
|
<span className="text-red-600">{importResult.failed}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 当前配置状态 */}
|
{/* 当前配置状态 */}
|
||||||
<Card className="border-blue-200 dark:border-blue-900 bg-blue-50/50 dark:bg-blue-950/20">
|
<Card className="border-blue-200 dark:border-blue-900 bg-blue-50/50 dark:bg-blue-950/20">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
@@ -737,12 +925,12 @@ const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) =>
|
|||||||
<DialogTitle>{t('relayStation.confirmDeleteTitle')}</DialogTitle>
|
<DialogTitle>{t('relayStation.confirmDeleteTitle')}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{t('relayStation.deleteConfirm')}
|
{t('relayStation.deleteConfirm')}
|
||||||
{stationToDelete && (
|
|
||||||
<div className="mt-2 p-2 bg-muted rounded">
|
|
||||||
<strong>{stationToDelete.name}</strong>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
|
{stationToDelete && (
|
||||||
|
<div className="mt-2 p-2 bg-muted rounded">
|
||||||
|
<strong>{stationToDelete.name}</strong>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
|
59
src/components/ui/alert.tsx
Normal file
59
src/components/ui/alert.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const alertVariants = cva(
|
||||||
|
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-background text-foreground",
|
||||||
|
destructive:
|
||||||
|
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const Alert = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||||
|
>(({ className, variant, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
role="alert"
|
||||||
|
className={cn(alertVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Alert.displayName = "Alert"
|
||||||
|
|
||||||
|
const AlertTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h5
|
||||||
|
ref={ref}
|
||||||
|
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertTitle.displayName = "AlertTitle"
|
||||||
|
|
||||||
|
const AlertDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDescription.displayName = "AlertDescription"
|
||||||
|
|
||||||
|
export { Alert, AlertTitle, AlertDescription }
|
26
src/components/ui/progress.tsx
Normal file
26
src/components/ui/progress.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Progress = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||||
|
>(({ className, value, ...props }, ref) => (
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ProgressPrimitive.Indicator
|
||||||
|
className="h-full w-full flex-1 bg-primary transition-all"
|
||||||
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
))
|
||||||
|
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Progress }
|
@@ -538,6 +538,15 @@ export interface ConnectionTestResult {
|
|||||||
error?: string; // 错误信息
|
error?: string; // 错误信息
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 导入结果统计 */
|
||||||
|
export interface ImportResult {
|
||||||
|
total: number; // 总数
|
||||||
|
imported: number; // 成功导入数
|
||||||
|
skipped: number; // 跳过数(重复)
|
||||||
|
failed: number; // 失败数
|
||||||
|
message: string; // 结果消息
|
||||||
|
}
|
||||||
|
|
||||||
/** Token 信息 */
|
/** Token 信息 */
|
||||||
export interface TokenInfo {
|
export interface TokenInfo {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -2271,6 +2280,39 @@ export const api = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports all relay stations configuration
|
||||||
|
* @returns Promise resolving to array of relay stations
|
||||||
|
*/
|
||||||
|
async relayStationsExport(): Promise<RelayStation[]> {
|
||||||
|
try {
|
||||||
|
return await invoke<RelayStation[]>("relay_stations_export");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to export relay stations:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports relay stations configuration
|
||||||
|
* @param stations - Array of relay stations to import
|
||||||
|
* @param clearExisting - Whether to clear existing stations before import
|
||||||
|
* @returns Promise resolving to success message
|
||||||
|
*/
|
||||||
|
async relayStationsImport(stations: CreateRelayStationRequest[], clearExisting: boolean = false): Promise<ImportResult> {
|
||||||
|
try {
|
||||||
|
return await invoke<ImportResult>("relay_stations_import", {
|
||||||
|
request: {
|
||||||
|
stations,
|
||||||
|
clear_existing: clearExisting
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to import relay stations:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets relay station information
|
* Gets relay station information
|
||||||
* @param stationId - The relay station ID
|
* @param stationId - The relay station ID
|
||||||
|
@@ -921,7 +921,20 @@
|
|||||||
"autoSelectFailed": "Failed to auto-select node",
|
"autoSelectFailed": "Failed to auto-select node",
|
||||||
"selectingBestNode": "Testing nodes to find the fastest...",
|
"selectingBestNode": "Testing nodes to find the fastest...",
|
||||||
"packycodeTokenNote": "PackyCode uses API Key authentication only",
|
"packycodeTokenNote": "PackyCode uses API Key authentication only",
|
||||||
"enabledNote": "Enable this station to make it available for use"
|
"enabledNote": "Enable this station to make it available for use",
|
||||||
|
"export": "Export Config",
|
||||||
|
"import": "Import Config",
|
||||||
|
"exportSuccess": "Relay stations exported successfully",
|
||||||
|
"exportFailed": "Failed to export",
|
||||||
|
"importSuccess": "Relay stations imported successfully",
|
||||||
|
"importFailed": "Failed to import",
|
||||||
|
"importConfirm": "Import {{count}} relay station(s)? This will not delete existing stations.",
|
||||||
|
"importing": "Importing...",
|
||||||
|
"importTotal": "Total",
|
||||||
|
"importSuccess": "Success",
|
||||||
|
"importSkipped": "Skipped (duplicate)",
|
||||||
|
"importFailed": "Failed",
|
||||||
|
"allDuplicate": "All configurations already exist, nothing imported"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"connected": "Connected",
|
"connected": "Connected",
|
||||||
|
@@ -848,7 +848,20 @@
|
|||||||
"autoSelectFailed": "自动选择节点失败",
|
"autoSelectFailed": "自动选择节点失败",
|
||||||
"selectingBestNode": "正在测试节点以寻找最快的...",
|
"selectingBestNode": "正在测试节点以寻找最快的...",
|
||||||
"packycodeTokenNote": "PackyCode 仅支持 API Key 认证方式",
|
"packycodeTokenNote": "PackyCode 仅支持 API Key 认证方式",
|
||||||
"enabledNote": "启用此中转站以使其可用"
|
"enabledNote": "启用此中转站以使其可用",
|
||||||
|
"export": "导出配置",
|
||||||
|
"import": "导入配置",
|
||||||
|
"exportSuccess": "中转站配置已导出",
|
||||||
|
"exportFailed": "导出失败",
|
||||||
|
"importSuccess": "中转站配置已导入",
|
||||||
|
"importFailed": "导入失败",
|
||||||
|
"importConfirm": "确认导入 {{count}} 个中转站配置?这不会删除现有配置。",
|
||||||
|
"importing": "正在导入...",
|
||||||
|
"importTotal": "总计",
|
||||||
|
"importSuccess": "成功",
|
||||||
|
"importSkipped": "跳过(重复)",
|
||||||
|
"importFailed": "失败",
|
||||||
|
"allDuplicate": "所有配置都已存在,未导入任何新配置"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"connected": "已连接",
|
"connected": "已连接",
|
||||||
|
Reference in New Issue
Block a user