2025-10-16 11:40:02 +08:00
|
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
|
import { toast } from "sonner";
|
|
|
|
|
import { settingsApi } from "@/lib/api";
|
|
|
|
|
|
2025-10-27 13:20:59 +08:00
|
|
|
export type ImportStatus =
|
|
|
|
|
| "idle"
|
|
|
|
|
| "importing"
|
|
|
|
|
| "success"
|
|
|
|
|
| "partial-success"
|
|
|
|
|
| "error";
|
2025-10-16 11:40:02 +08:00
|
|
|
|
|
|
|
|
export interface UseImportExportOptions {
|
|
|
|
|
onImportSuccess?: () => void | Promise<void>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface UseImportExportResult {
|
|
|
|
|
selectedFile: string;
|
|
|
|
|
status: ImportStatus;
|
|
|
|
|
errorMessage: string | null;
|
|
|
|
|
backupId: string | null;
|
|
|
|
|
isImporting: boolean;
|
|
|
|
|
selectImportFile: () => Promise<void>;
|
|
|
|
|
clearSelection: () => void;
|
|
|
|
|
importConfig: () => Promise<void>;
|
|
|
|
|
exportConfig: () => Promise<void>;
|
|
|
|
|
resetStatus: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useImportExport(
|
|
|
|
|
options: UseImportExportOptions = {},
|
|
|
|
|
): UseImportExportResult {
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
const { onImportSuccess } = options;
|
|
|
|
|
|
|
|
|
|
const [selectedFile, setSelectedFile] = useState("");
|
|
|
|
|
const [status, setStatus] = useState<ImportStatus>("idle");
|
|
|
|
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
|
|
|
const [backupId, setBackupId] = useState<string | null>(null);
|
|
|
|
|
const [isImporting, setIsImporting] = useState(false);
|
|
|
|
|
const successTimerRef = useRef<number | null>(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
return () => {
|
|
|
|
|
if (successTimerRef.current) {
|
|
|
|
|
window.clearTimeout(successTimerRef.current);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const clearSelection = useCallback(() => {
|
|
|
|
|
setSelectedFile("");
|
|
|
|
|
setStatus("idle");
|
|
|
|
|
setErrorMessage(null);
|
|
|
|
|
setBackupId(null);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const selectImportFile = useCallback(async () => {
|
|
|
|
|
try {
|
|
|
|
|
const filePath = await settingsApi.openFileDialog();
|
|
|
|
|
if (filePath) {
|
|
|
|
|
setSelectedFile(filePath);
|
|
|
|
|
setStatus("idle");
|
|
|
|
|
setErrorMessage(null);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("[useImportExport] Failed to open file dialog", error);
|
|
|
|
|
toast.error(
|
|
|
|
|
t("settings.selectFileFailed", {
|
|
|
|
|
defaultValue: "选择文件失败",
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}, [t]);
|
|
|
|
|
|
|
|
|
|
const importConfig = useCallback(async () => {
|
|
|
|
|
if (!selectedFile) {
|
|
|
|
|
toast.error(
|
|
|
|
|
t("settings.selectFileFailed", {
|
|
|
|
|
defaultValue: "请选择有效的配置文件",
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isImporting) return;
|
|
|
|
|
|
|
|
|
|
setIsImporting(true);
|
|
|
|
|
setStatus("importing");
|
|
|
|
|
setErrorMessage(null);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const result = await settingsApi.importConfigFromFile(selectedFile);
|
2025-10-27 13:20:59 +08:00
|
|
|
if (!result.success) {
|
|
|
|
|
setStatus("error");
|
|
|
|
|
const message =
|
|
|
|
|
result.message ||
|
|
|
|
|
t("settings.configCorrupted", {
|
|
|
|
|
defaultValue: "配置文件已损坏或格式不正确",
|
|
|
|
|
});
|
|
|
|
|
setErrorMessage(message);
|
|
|
|
|
toast.error(message);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setBackupId(result.backupId ?? null);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await settingsApi.syncCurrentProvidersLive();
|
2025-10-16 11:40:02 +08:00
|
|
|
setStatus("success");
|
|
|
|
|
toast.success(
|
|
|
|
|
t("settings.importSuccess", {
|
|
|
|
|
defaultValue: "配置导入成功",
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
successTimerRef.current = window.setTimeout(() => {
|
|
|
|
|
void onImportSuccess?.();
|
|
|
|
|
}, 1500);
|
2025-10-27 13:20:59 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error("[useImportExport] Failed to sync live config", error);
|
|
|
|
|
setStatus("partial-success");
|
|
|
|
|
toast.warning(
|
|
|
|
|
t("settings.importPartialSuccess", {
|
|
|
|
|
defaultValue:
|
|
|
|
|
"配置已导入,但同步到当前供应商失败。请手动重新选择一次供应商。",
|
|
|
|
|
}),
|
|
|
|
|
);
|
2025-10-16 11:40:02 +08:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("[useImportExport] Failed to import config", error);
|
|
|
|
|
setStatus("error");
|
|
|
|
|
const message =
|
|
|
|
|
error instanceof Error ? error.message : String(error ?? "");
|
|
|
|
|
setErrorMessage(message);
|
|
|
|
|
toast.error(
|
|
|
|
|
t("settings.importFailedError", {
|
|
|
|
|
defaultValue: "导入配置失败: {{message}}",
|
|
|
|
|
message,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsImporting(false);
|
|
|
|
|
}
|
|
|
|
|
}, [isImporting, onImportSuccess, selectedFile, t]);
|
|
|
|
|
|
|
|
|
|
const exportConfig = useCallback(async () => {
|
|
|
|
|
try {
|
|
|
|
|
const defaultName = `cc-switch-config-${
|
|
|
|
|
new Date().toISOString().split("T")[0]
|
|
|
|
|
}.json`;
|
|
|
|
|
const destination = await settingsApi.saveFileDialog(defaultName);
|
|
|
|
|
if (!destination) {
|
|
|
|
|
toast.error(
|
|
|
|
|
t("settings.selectFileFailed", {
|
|
|
|
|
defaultValue: "选择保存位置失败",
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await settingsApi.exportConfigToFile(destination);
|
|
|
|
|
if (result.success) {
|
|
|
|
|
const displayPath = result.filePath ?? destination;
|
|
|
|
|
toast.success(
|
|
|
|
|
t("settings.configExported", {
|
|
|
|
|
defaultValue: "配置已导出",
|
|
|
|
|
}) + `\n${displayPath}`,
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
toast.error(
|
|
|
|
|
t("settings.exportFailed", {
|
|
|
|
|
defaultValue: "导出配置失败",
|
|
|
|
|
}) + (result.message ? `: ${result.message}` : ""),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("[useImportExport] Failed to export config", error);
|
|
|
|
|
toast.error(
|
|
|
|
|
t("settings.exportFailedError", {
|
|
|
|
|
defaultValue: "导出配置失败: {{message}}",
|
|
|
|
|
message: error instanceof Error ? error.message : String(error ?? ""),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}, [t]);
|
|
|
|
|
|
|
|
|
|
const resetStatus = useCallback(() => {
|
|
|
|
|
setStatus("idle");
|
|
|
|
|
setErrorMessage(null);
|
|
|
|
|
setBackupId(null);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
selectedFile,
|
|
|
|
|
status,
|
|
|
|
|
errorMessage,
|
|
|
|
|
backupId,
|
|
|
|
|
isImporting,
|
|
|
|
|
selectImportFile,
|
|
|
|
|
clearSelection,
|
|
|
|
|
importConfig,
|
|
|
|
|
exportConfig,
|
|
|
|
|
resetStatus,
|
|
|
|
|
};
|
|
|
|
|
}
|