Files
cc-switch/src/hooks/useImportExport.ts

211 lines
6.0 KiB
TypeScript
Raw Normal View History

import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { settingsApi } from "@/lib/api";
import { syncCurrentProvidersLiveSafe } from "@/utils/postChangeSync";
export type ImportStatus =
| "idle"
| "importing"
| "success"
| "partial-success"
| "error";
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: "请选择有效的 SQL 备份文件",
}),
);
return;
}
if (isImporting) return;
setIsImporting(true);
setStatus("importing");
setErrorMessage(null);
try {
const result = await settingsApi.importConfigFromFile(selectedFile);
if (!result.success) {
setStatus("error");
const message =
result.message ||
t("settings.configCorrupted", {
defaultValue: "SQL 文件已损坏或格式不正确",
});
setErrorMessage(message);
toast.error(message);
return;
}
setBackupId(result.backupId ?? null);
const syncResult = await syncCurrentProvidersLiveSafe();
if (syncResult.ok) {
setStatus("success");
toast.success(
t("settings.importSuccess", {
defaultValue: "配置导入成功",
}),
);
successTimerRef.current = window.setTimeout(() => {
void onImportSuccess?.();
}, 1500);
} else {
console.error(
"[useImportExport] Failed to sync live config",
syncResult.error,
);
setStatus("partial-success");
toast.warning(
t("settings.importPartialSuccess", {
defaultValue:
"配置已导入,但同步到当前供应商失败。请手动重新选择一次供应商。",
}),
);
}
} 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 now = new Date();
const stamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}_${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}${String(now.getSeconds()).padStart(2, "0")}`;
const defaultName = `cc-switch-export-${stamp}.sql`;
const destination = await settingsApi.saveFileDialog(defaultName);
if (!destination) {
toast.error(
t("settings.selectFileFailed", {
defaultValue: "请选择 SQL 备份保存路径",
}),
);
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,
};
}