add: local config import and export (#84)
* add: local config import and export * Fix import refresh flow and typings * Clarify import refresh messaging * Limit stored import backups --------- Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
3
src-tauri/Cargo.lock
generated
3
src-tauri/Cargo.lock
generated
@@ -565,6 +565,7 @@ dependencies = [
|
||||
name = "cc-switch"
|
||||
version = "3.4.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs 5.0.1",
|
||||
"log",
|
||||
"objc2 0.5.2",
|
||||
@@ -628,8 +629,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link 0.2.0",
|
||||
]
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ tauri-build = { version = "2.4.0", features = [] }
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
log = "0.4"
|
||||
chrono = "0.4"
|
||||
tauri = { version = "2.8.2", features = ["tray-icon"] }
|
||||
tauri-plugin-log = "2"
|
||||
tauri-plugin-opener = "2"
|
||||
|
||||
170
src-tauri/src/import_export.rs
Normal file
170
src-tauri/src/import_export.rs
Normal file
@@ -0,0 +1,170 @@
|
||||
use chrono::Utc;
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// 默认仅保留最近 10 份备份,避免目录无限膨胀
|
||||
const MAX_BACKUPS: usize = 10;
|
||||
|
||||
/// 创建配置文件备份
|
||||
pub fn create_backup(config_path: &PathBuf) -> Result<String, String> {
|
||||
if !config_path.exists() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
|
||||
let backup_id = format!("backup_{}", timestamp);
|
||||
|
||||
let backup_dir = config_path
|
||||
.parent()
|
||||
.ok_or("Invalid config path")?
|
||||
.join("backups");
|
||||
|
||||
// 创建备份目录
|
||||
fs::create_dir_all(&backup_dir)
|
||||
.map_err(|e| format!("Failed to create backup directory: {}", e))?;
|
||||
|
||||
let backup_path = backup_dir.join(format!("{}.json", backup_id));
|
||||
|
||||
// 复制配置文件到备份
|
||||
fs::copy(config_path, backup_path).map_err(|e| format!("Failed to create backup: {}", e))?;
|
||||
|
||||
// 备份完成后清理旧的备份文件(仅保留最近 MAX_BACKUPS 份)
|
||||
cleanup_old_backups(&backup_dir, MAX_BACKUPS)?;
|
||||
|
||||
Ok(backup_id)
|
||||
}
|
||||
|
||||
fn cleanup_old_backups(backup_dir: &PathBuf, retain: usize) -> Result<(), String> {
|
||||
if retain == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut entries: Vec<_> = match fs::read_dir(backup_dir) {
|
||||
Ok(iter) => iter
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| {
|
||||
entry
|
||||
.path()
|
||||
.extension()
|
||||
.map(|ext| ext == "json")
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect(),
|
||||
Err(_) => return Ok(()),
|
||||
};
|
||||
|
||||
if entries.len() <= retain {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let remove_count = entries.len().saturating_sub(retain);
|
||||
|
||||
entries.sort_by(|a, b| {
|
||||
let a_time = a.metadata().and_then(|m| m.modified()).ok();
|
||||
let b_time = b.metadata().and_then(|m| m.modified()).ok();
|
||||
a_time.cmp(&b_time)
|
||||
});
|
||||
|
||||
for entry in entries.into_iter().take(remove_count) {
|
||||
if let Err(err) = fs::remove_file(entry.path()) {
|
||||
log::warn!(
|
||||
"Failed to remove old backup {}: {}",
|
||||
entry.path().display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 导出配置文件
|
||||
#[tauri::command]
|
||||
pub async fn export_config_to_file(file_path: String) -> Result<Value, String> {
|
||||
// 读取当前配置文件
|
||||
let config_path = crate::config::get_app_config_path();
|
||||
let config_content = fs::read_to_string(&config_path)
|
||||
.map_err(|e| format!("Failed to read configuration: {}", e))?;
|
||||
|
||||
// 写入到指定文件
|
||||
fs::write(&file_path, &config_content).map_err(|e| format!("Failed to write file: {}", e))?;
|
||||
|
||||
Ok(json!({
|
||||
"success": true,
|
||||
"message": "Configuration exported successfully",
|
||||
"filePath": file_path
|
||||
}))
|
||||
}
|
||||
|
||||
/// 从文件导入配置
|
||||
#[tauri::command]
|
||||
pub async fn import_config_from_file(
|
||||
file_path: String,
|
||||
state: tauri::State<'_, crate::store::AppState>,
|
||||
) -> Result<Value, String> {
|
||||
// 读取导入的文件
|
||||
let import_content =
|
||||
fs::read_to_string(&file_path).map_err(|e| format!("Failed to read import file: {}", e))?;
|
||||
|
||||
// 验证并解析为配置对象
|
||||
let new_config: crate::app_config::MultiAppConfig = serde_json::from_str(&import_content)
|
||||
.map_err(|e| format!("Invalid configuration file: {}", e))?;
|
||||
|
||||
// 备份当前配置
|
||||
let config_path = crate::config::get_app_config_path();
|
||||
let backup_id = create_backup(&config_path)?;
|
||||
|
||||
// 写入新配置到磁盘
|
||||
fs::write(&config_path, &import_content)
|
||||
.map_err(|e| format!("Failed to write configuration: {}", e))?;
|
||||
|
||||
// 更新内存中的状态
|
||||
{
|
||||
let mut config_state = state
|
||||
.config
|
||||
.lock()
|
||||
.map_err(|e| format!("Failed to lock config: {}", e))?;
|
||||
*config_state = new_config;
|
||||
}
|
||||
|
||||
Ok(json!({
|
||||
"success": true,
|
||||
"message": "Configuration imported successfully",
|
||||
"backupId": backup_id
|
||||
}))
|
||||
}
|
||||
|
||||
/// 保存文件对话框
|
||||
#[tauri::command]
|
||||
pub async fn save_file_dialog<R: tauri::Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
default_name: String,
|
||||
) -> Result<Option<String>, String> {
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
|
||||
let dialog = app.dialog();
|
||||
let result = dialog
|
||||
.file()
|
||||
.add_filter("JSON", &["json"])
|
||||
.set_file_name(&default_name)
|
||||
.blocking_save_file();
|
||||
|
||||
Ok(result.map(|p| p.to_string()))
|
||||
}
|
||||
|
||||
/// 打开文件对话框
|
||||
#[tauri::command]
|
||||
pub async fn open_file_dialog<R: tauri::Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
) -> Result<Option<String>, String> {
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
|
||||
let dialog = app.dialog();
|
||||
let result = dialog
|
||||
.file()
|
||||
.add_filter("JSON", &["json"])
|
||||
.blocking_pick_file();
|
||||
|
||||
Ok(result.map(|p| p.to_string()))
|
||||
}
|
||||
@@ -3,6 +3,7 @@ mod claude_plugin;
|
||||
mod codex_config;
|
||||
mod commands;
|
||||
mod config;
|
||||
mod import_export;
|
||||
mod migration;
|
||||
mod provider;
|
||||
mod settings;
|
||||
@@ -419,6 +420,10 @@ pub fn run() {
|
||||
commands::read_claude_plugin_config,
|
||||
commands::apply_claude_plugin_config,
|
||||
commands::is_claude_plugin_applied,
|
||||
import_export::export_config_to_file,
|
||||
import_export::import_config_from_file,
|
||||
import_export::save_file_dialog,
|
||||
import_export::open_file_dialog,
|
||||
update_tray_menu,
|
||||
]);
|
||||
|
||||
|
||||
14
src/App.tsx
14
src/App.tsx
@@ -229,6 +229,15 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportSuccess = async () => {
|
||||
await loadProviders();
|
||||
try {
|
||||
await window.api.updateTrayMenu();
|
||||
} catch (error) {
|
||||
console.error("[App] Failed to refresh tray menu after import", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 自动从 live 导入一条默认供应商(仅首次初始化时)
|
||||
const handleAutoImportDefault = async () => {
|
||||
try {
|
||||
@@ -357,7 +366,10 @@ function App() {
|
||||
)}
|
||||
|
||||
{isSettingsOpen && (
|
||||
<SettingsModal onClose={() => setIsSettingsOpen(false)} />
|
||||
<SettingsModal
|
||||
onClose={() => setIsSettingsOpen(false)}
|
||||
onImportSuccess={handleImportSuccess}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
103
src/components/ImportProgressModal.tsx
Normal file
103
src/components/ImportProgressModal.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { useEffect } from "react";
|
||||
import { CheckCircle, Loader2, AlertCircle } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface ImportProgressModalProps {
|
||||
status: 'importing' | 'success' | 'error';
|
||||
message?: string;
|
||||
backupId?: string;
|
||||
onComplete?: () => void;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
export function ImportProgressModal({
|
||||
status,
|
||||
message,
|
||||
backupId,
|
||||
onComplete,
|
||||
onSuccess
|
||||
}: ImportProgressModalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'success') {
|
||||
console.log('[ImportProgressModal] Success detected, starting 2 second countdown');
|
||||
// 成功后等待2秒自动关闭并刷新数据
|
||||
const timer = setTimeout(() => {
|
||||
console.log('[ImportProgressModal] 2 seconds elapsed, calling callbacks...');
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
return () => {
|
||||
console.log('[ImportProgressModal] Cleanup timer');
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}
|
||||
}, [status, onComplete, onSuccess]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black/50 dark:bg-black/70 backdrop-blur-sm" />
|
||||
|
||||
<div className="relative bg-white dark:bg-gray-900 rounded-xl shadow-2xl p-8 max-w-md w-full mx-4">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
{status === 'importing' && (
|
||||
<>
|
||||
<Loader2 className="w-12 h-12 text-blue-500 animate-spin mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||
{t("settings.importing")}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t("common.loading")}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{status === 'success' && (
|
||||
<>
|
||||
<CheckCircle className="w-12 h-12 text-green-500 mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||
{t("settings.importSuccess")}
|
||||
</h3>
|
||||
{backupId && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||
{t("settings.backupId")}: {backupId}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t("settings.autoReload")}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{status === 'error' && (
|
||||
<>
|
||||
<AlertCircle className="w-12 h-12 text-red-500 mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||
{t("settings.importFailed")}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{message || t("settings.configCorrupted")}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
}}
|
||||
className="mt-4 px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors"
|
||||
>
|
||||
{t("common.close")}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Save,
|
||||
} from "lucide-react";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { ImportProgressModal } from "./ImportProgressModal";
|
||||
import { homeDir, join } from "@tauri-apps/api/path";
|
||||
import "../lib/tauri-api";
|
||||
import { relaunchApp } from "../lib/updater";
|
||||
@@ -22,9 +23,10 @@ import { isLinux } from "../lib/platform";
|
||||
|
||||
interface SettingsModalProps {
|
||||
onClose: () => void;
|
||||
onImportSuccess?: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
export default function SettingsModal({ onClose, onImportSuccess }: SettingsModalProps) {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const normalizeLanguage = (lang?: string | null): "zh" | "en" =>
|
||||
@@ -63,6 +65,13 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
const { hasUpdate, updateInfo, updateHandle, checkUpdate, resetDismiss } =
|
||||
useUpdate();
|
||||
|
||||
// 导入/导出相关状态
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const [importStatus, setImportStatus] = useState<'idle' | 'importing' | 'success' | 'error'>('idle');
|
||||
const [importError, setImportError] = useState<string>("");
|
||||
const [importBackupId, setImportBackupId] = useState<string>("");
|
||||
const [selectedImportFile, setSelectedImportFile] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
loadConfigPath();
|
||||
@@ -346,6 +355,66 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
}
|
||||
};
|
||||
|
||||
// 导出配置处理函数
|
||||
const handleExportConfig = async () => {
|
||||
try {
|
||||
const defaultName = `cc-switch-config-${new Date().toISOString().split('T')[0]}.json`;
|
||||
const filePath = await window.api.saveFileDialog(defaultName);
|
||||
|
||||
if (!filePath) return; // 用户取消了
|
||||
|
||||
const result = await window.api.exportConfigToFile(filePath);
|
||||
|
||||
if (result.success) {
|
||||
alert(`${t("settings.configExported")}\n${result.filePath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("导出配置失败:", error);
|
||||
alert(`${t("settings.exportFailed")}: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 选择要导入的文件
|
||||
const handleSelectImportFile = async () => {
|
||||
try {
|
||||
const filePath = await window.api.openFileDialog();
|
||||
if (filePath) {
|
||||
setSelectedImportFile(filePath);
|
||||
setImportStatus('idle'); // 重置状态
|
||||
setImportError('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('选择文件失败:', error);
|
||||
alert(`${t("settings.selectFileFailed")}: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 执行导入
|
||||
const handleExecuteImport = async () => {
|
||||
if (!selectedImportFile || isImporting) return;
|
||||
|
||||
setIsImporting(true);
|
||||
setImportStatus('importing');
|
||||
|
||||
try {
|
||||
const result = await window.api.importConfigFromFile(selectedImportFile);
|
||||
|
||||
if (result.success) {
|
||||
setImportBackupId(result.backupId || '');
|
||||
setImportStatus('success');
|
||||
// ImportProgressModal 会在2秒后触发数据刷新回调
|
||||
} else {
|
||||
setImportError(result.message || t("settings.configCorrupted"));
|
||||
setImportStatus('error');
|
||||
}
|
||||
} catch (error) {
|
||||
setImportError(String(error));
|
||||
setImportStatus('error');
|
||||
} finally {
|
||||
setIsImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||
@@ -542,6 +611,56 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 导入导出 */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||
{t("settings.importExport")}
|
||||
</h3>
|
||||
<div className="p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
|
||||
<div className="space-y-3">
|
||||
{/* 导出按钮 */}
|
||||
<button
|
||||
onClick={handleExportConfig}
|
||||
className="w-full flex items-center justify-center gap-2 px-3 py-2 text-xs font-medium rounded-lg transition-colors bg-gray-500 hover:bg-gray-600 dark:bg-gray-600 dark:hover:bg-gray-700 text-white"
|
||||
>
|
||||
<Save size={12} />
|
||||
{t("settings.exportConfig")}
|
||||
</button>
|
||||
|
||||
{/* 导入区域 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleSelectImportFile}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 text-xs font-medium rounded-lg transition-colors bg-gray-500 hover:bg-gray-600 dark:bg-gray-600 dark:hover:bg-gray-700 text-white"
|
||||
>
|
||||
<FolderOpen size={12} />
|
||||
{t("settings.selectConfigFile")}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleExecuteImport}
|
||||
disabled={!selectedImportFile || isImporting}
|
||||
className={`px-3 py-2 text-xs font-medium rounded-lg transition-colors text-white ${
|
||||
!selectedImportFile || isImporting
|
||||
? 'bg-gray-400 cursor-not-allowed'
|
||||
: 'bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700'
|
||||
}`}
|
||||
>
|
||||
{isImporting ? t("settings.importing") : t("settings.import")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 显示选择的文件 */}
|
||||
{selectedImportFile && (
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 px-2 py-1 bg-gray-50 dark:bg-gray-900 rounded break-all">
|
||||
{selectedImportFile.split('/').pop() || selectedImportFile.split('\\').pop() || selectedImportFile}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 关于 */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||
@@ -636,6 +755,28 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Import Progress Modal */}
|
||||
{importStatus !== 'idle' && (
|
||||
<ImportProgressModal
|
||||
status={importStatus}
|
||||
message={importError}
|
||||
backupId={importBackupId}
|
||||
onComplete={() => {
|
||||
setImportStatus('idle');
|
||||
setImportError('');
|
||||
setSelectedImportFile('');
|
||||
}}
|
||||
onSuccess={() => {
|
||||
if (onImportSuccess) {
|
||||
void onImportSuccess();
|
||||
}
|
||||
void window.api
|
||||
.updateTrayMenu()
|
||||
.catch((error) => console.error("[SettingsModal] Failed to refresh tray menu", error));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,6 +61,19 @@
|
||||
"title": "Settings",
|
||||
"general": "General",
|
||||
"language": "Language",
|
||||
"importExport": "Import/Export Config",
|
||||
"exportConfig": "Export Config to File",
|
||||
"selectConfigFile": "Select Config File",
|
||||
"import": "Import",
|
||||
"importing": "Importing...",
|
||||
"importSuccess": "Import Successful!",
|
||||
"importFailed": "Import Failed",
|
||||
"configExported": "Config exported to:",
|
||||
"exportFailed": "Export failed",
|
||||
"selectFileFailed": "Failed to select file",
|
||||
"configCorrupted": "Config file may be corrupted or invalid",
|
||||
"backupId": "Backup ID",
|
||||
"autoReload": "Data will refresh automatically in 2 seconds...",
|
||||
"languageOptionChinese": "中文",
|
||||
"languageOptionEnglish": "English",
|
||||
"windowBehavior": "Window Behavior",
|
||||
|
||||
@@ -61,6 +61,19 @@
|
||||
"title": "设置",
|
||||
"general": "通用",
|
||||
"language": "界面语言",
|
||||
"importExport": "导入导出配置",
|
||||
"exportConfig": "导出配置到文件",
|
||||
"selectConfigFile": "选择配置文件",
|
||||
"import": "导入",
|
||||
"importing": "导入中...",
|
||||
"importSuccess": "导入成功!",
|
||||
"importFailed": "导入失败",
|
||||
"configExported": "配置已导出到:",
|
||||
"exportFailed": "导出失败",
|
||||
"selectFileFailed": "选择文件失败",
|
||||
"configCorrupted": "配置文件可能已损坏或格式不正确",
|
||||
"backupId": "备份ID",
|
||||
"autoReload": "数据将在2秒后自动刷新...",
|
||||
"languageOptionChinese": "中文",
|
||||
"languageOptionEnglish": "English",
|
||||
"windowBehavior": "窗口行为",
|
||||
|
||||
@@ -312,6 +312,54 @@ export const tauriAPI = {
|
||||
throw new Error(`检测 Claude 插件配置失败: ${String(error)}`);
|
||||
}
|
||||
},
|
||||
|
||||
// 导出配置到文件
|
||||
exportConfigToFile: async (filePath: string): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
filePath: string;
|
||||
}> => {
|
||||
try {
|
||||
return await invoke("export_config_to_file", { filePath });
|
||||
} catch (error) {
|
||||
throw new Error(`导出配置失败: ${String(error)}`);
|
||||
}
|
||||
},
|
||||
|
||||
// 从文件导入配置
|
||||
importConfigFromFile: async (filePath: string): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
backupId?: string;
|
||||
}> => {
|
||||
try {
|
||||
return await invoke("import_config_from_file", { filePath });
|
||||
} catch (error) {
|
||||
throw new Error(`导入配置失败: ${String(error)}`);
|
||||
}
|
||||
},
|
||||
|
||||
// 保存文件对话框
|
||||
saveFileDialog: async (defaultName: string): Promise<string | null> => {
|
||||
try {
|
||||
const result = await invoke<string | null>("save_file_dialog", { defaultName });
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("打开保存对话框失败:", error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// 打开文件对话框
|
||||
openFileDialog: async (): Promise<string | null> => {
|
||||
try {
|
||||
const result = await invoke<string | null>("open_file_dialog");
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("打开文件对话框失败:", error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// 创建全局 API 对象,兼容现有代码
|
||||
|
||||
12
src/vite-env.d.ts
vendored
12
src/vite-env.d.ts
vendored
@@ -29,6 +29,18 @@ declare global {
|
||||
getClaudeConfigStatus: () => Promise<ConfigStatus>;
|
||||
getConfigStatus: (app?: AppType) => Promise<ConfigStatus>;
|
||||
getConfigDir: (app?: AppType) => Promise<string>;
|
||||
saveFileDialog: (defaultName: string) => Promise<string | null>;
|
||||
openFileDialog: () => Promise<string | null>;
|
||||
exportConfigToFile: (filePath: string) => Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
filePath: string;
|
||||
}>;
|
||||
importConfigFromFile: (filePath: string) => Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
backupId?: string;
|
||||
}>;
|
||||
selectConfigDirectory: (defaultPath?: string) => Promise<string | null>;
|
||||
openConfigFolder: (app?: AppType) => Promise<void>;
|
||||
openExternal: (url: string) => Promise<void>;
|
||||
|
||||
Reference in New Issue
Block a user