fix(ui): add close button to all success toasts

This commit is contained in:
Jason
2025-12-16 10:56:39 +08:00
parent 510a013449
commit d4f33224c6
29 changed files with 98 additions and 56 deletions
+2 -1
View File
@@ -142,6 +142,7 @@ function App() {
if (migrated) {
toast.success(
t("migration.success", { defaultValue: "配置迁移成功" }),
{ closeButton: true },
);
}
} catch (error) {
@@ -422,7 +423,7 @@ function App() {
"text-xl font-semibold transition-colors",
isProxyRunning && isTakeoverActive
? "text-emerald-500 hover:text-emerald-600 dark:text-emerald-400 dark:hover:text-emerald-300"
: "text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"
: "text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300",
)}
>
CC Switch
+13 -8
View File
@@ -32,7 +32,7 @@ export function DeepLinkImportDialog() {
// 容错判断:MCP 导入结果可能缺少 type 字段
const isMcpImportResult = (
value: unknown
value: unknown,
): value is {
importedCount: number;
importedIds: string[];
@@ -59,7 +59,7 @@ export function DeepLinkImportDialog() {
if (event.payload.config || event.payload.configUrl) {
try {
const mergedRequest = await deeplinkApi.mergeDeeplinkConfig(
event.payload
event.payload,
);
console.log("Config merged successfully:", mergedRequest);
setRequest(mergedRequest);
@@ -77,7 +77,7 @@ export function DeepLinkImportDialog() {
}
setIsOpen(true);
}
},
);
// Listen for deep link error events
@@ -128,6 +128,7 @@ export function DeepLinkImportDialog() {
description: t("deeplink.mcpImportSuccessDescription", {
count: summary.importedCount,
}),
closeButton: true,
});
}
};
@@ -142,18 +143,20 @@ export function DeepLinkImportDialog() {
description: t("deeplink.importSuccessDescription", {
name: request.name,
}),
closeButton: true,
});
} else if (result.type === "prompt") {
// Prompts don't use React Query, trigger a custom event for refresh
window.dispatchEvent(
new CustomEvent("prompt-imported", {
detail: { app: request.app },
})
}),
);
toast.success(t("deeplink.promptImportSuccess"), {
description: t("deeplink.promptImportSuccessDescription", {
name: request.name,
}),
closeButton: true,
});
} else if (result.type === "mcp") {
await refreshMcp(result);
@@ -171,6 +174,7 @@ export function DeepLinkImportDialog() {
description: t("deeplink.skillImportSuccessDescription", {
repo: request.repo,
}),
closeButton: true,
});
}
} else if (isMcpImportResult(result)) {
@@ -185,6 +189,7 @@ export function DeepLinkImportDialog() {
description: t("deeplink.importSuccessDescription", {
name: request.name,
}),
closeButton: true,
});
}
@@ -279,7 +284,7 @@ export function DeepLinkImportDialog() {
const maskValue = (key: string, value: string): string => {
const sensitiveKeys = ["TOKEN", "KEY", "SECRET", "PASSWORD"];
const isSensitive = sensitiveKeys.some((k) =>
key.toUpperCase().includes(k)
key.toUpperCase().includes(k),
);
if (isSensitive && value.length > 8) {
return `${value.substring(0, 8)}${"*".repeat(12)}`;
@@ -521,7 +526,7 @@ export function DeepLinkImportDialog() {
{maskValue(key, String(value))}
</span>
</div>
)
),
)}
</div>
)}
@@ -548,7 +553,7 @@ export function DeepLinkImportDialog() {
{maskValue(key, String(value))}
</span>
</div>
)
),
)}
</div>
)}
@@ -584,7 +589,7 @@ export function DeepLinkImportDialog() {
{maskValue(key, String(value))}
</span>
</div>
)
),
)}
</div>
)}
+3 -1
View File
@@ -234,7 +234,9 @@ const JsonEditor: React.FC<JsonEditorProps> = ({
try {
const formatted = formatJSON(currentValue);
onChange(formatted);
toast.success(t("common.formatSuccess", { defaultValue: "格式化成功" }));
toast.success(t("common.formatSuccess", { defaultValue: "格式化成功" }), {
closeButton: true,
});
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
+5 -1
View File
@@ -227,6 +227,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
.join(", ");
toast.success(`${t("usageScript.testSuccess")}${summary}`, {
duration: 3000,
closeButton: true,
});
} else {
toast.error(
@@ -259,7 +260,10 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
printWidth: 80,
});
setScript({ ...script, code: formatted.trim() });
toast.success(t("usageScript.formatSuccess"), { duration: 1000 });
toast.success(t("usageScript.formatSuccess"), {
duration: 1000,
closeButton: true,
});
} catch (error: any) {
toast.error(
`${t("usageScript.formatFailed")}: ${error?.message || t("jsonEditor.invalidJson")}`,
+1
View File
@@ -79,6 +79,7 @@ export function EnvWarningBanner({
path: backupInfo.backupPath,
}),
duration: 5000,
closeButton: true,
});
// 清空选择并通知父组件
+1 -1
View File
@@ -391,7 +391,7 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
}
await upsertMutation.mutateAsync(entry);
toast.success(t("common.success"));
toast.success(t("common.success"), { closeButton: true });
await onSave();
} catch (error: any) {
const detail = extractErrorMessage(error);
+1 -1
View File
@@ -99,7 +99,7 @@ const UnifiedMcpPanel = React.forwardRef<
try {
await deleteServerMutation.mutateAsync(id);
setConfirmDialog(null);
toast.success(t("common.success"));
toast.success(t("common.success"), { closeButton: true });
} catch (error) {
toast.error(t("common.error"), {
description: String(error),
+2 -2
View File
@@ -59,7 +59,7 @@ export function ProviderActions({
// 代理接管模式下启用按钮使用绿色
!isCurrent &&
isProxyTakeover &&
"bg-emerald-500 hover:bg-emerald-600 dark:bg-emerald-600 dark:hover:bg-emerald-700"
"bg-emerald-500 hover:bg-emerald-600 dark:bg-emerald-600 dark:hover:bg-emerald-700",
)}
>
{isCurrent ? (
@@ -158,7 +158,7 @@ export function ProviderActions({
className={cn(
iconButtonClass,
!isCurrent && "hover:text-red-500 dark:hover:text-red-400",
isCurrent && "opacity-40 cursor-not-allowed text-muted-foreground"
isCurrent && "opacity-40 cursor-not-allowed text-muted-foreground",
)}
>
<Trash2 className="h-4 w-4" />
+14 -7
View File
@@ -111,6 +111,7 @@ export function ProviderCard({
t("provider.circuitBreakerReset", {
defaultValue: "熔断器已重置",
}),
{ closeButton: true },
);
} catch (error) {
toast.error(
@@ -183,7 +184,9 @@ export function ProviderCard({
"relative overflow-hidden rounded-xl border border-border p-4 transition-all duration-300",
"bg-card text-card-foreground group",
// 代理接管模式下 hover 使用绿色边框,否则使用蓝色
isProxyTakeover ? "hover:border-emerald-500/50" : "hover:border-border-active",
isProxyTakeover
? "hover:border-emerald-500/50"
: "hover:border-border-active",
// 代理接管模式下当前供应商使用绿色边框
isProxyTakeover && isCurrent
? "border-emerald-500/60 shadow-sm shadow-emerald-500/10"
@@ -194,12 +197,16 @@ export function ProviderCard({
"cursor-grabbing border-primary shadow-lg scale-105 z-10",
)}
>
<div className={cn(
"absolute inset-0 bg-gradient-to-r to-transparent transition-opacity duration-500 pointer-events-none",
// 代理接管模式下使用绿色渐变,否则使用蓝色主色调
isProxyTakeover && isCurrent ? "from-emerald-500/10" : "from-primary/10",
isCurrent ? "opacity-100" : "opacity-0"
)} />
<div
className={cn(
"absolute inset-0 bg-gradient-to-r to-transparent transition-opacity duration-500 pointer-events-none",
// 代理接管模式下使用绿色渐变,否则使用蓝色主色调
isProxyTakeover && isCurrent
? "from-emerald-500/10"
: "from-primary/10",
isCurrent ? "opacity-100" : "opacity-0",
)}
/>
<div className="relative flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-1 items-center gap-2">
<button
@@ -54,6 +54,7 @@ export function AutoFailoverConfigPanel({
});
toast.success(
t("proxy.autoFailover.configSaved", "自动故障转移配置已保存"),
{ closeButton: true },
);
} catch (e) {
toast.error(
@@ -34,7 +34,7 @@ export function CircuitBreakerConfigPanel() {
const handleSave = async () => {
try {
await updateConfig.mutateAsync(formData);
toast.success("熔断器配置已保存");
toast.success("熔断器配置已保存", { closeButton: true });
} catch (error) {
toast.error("保存失败: " + String(error));
}
+13 -12
View File
@@ -73,10 +73,8 @@ export function FailoverQueueManager({
isLoading: isQueueLoading,
error: queueError,
} = useFailoverQueue(appType);
const {
data: availableProviders,
isLoading: isProvidersLoading,
} = useAvailableProvidersForFailover(appType);
const { data: availableProviders, isLoading: isProvidersLoading } =
useAvailableProvidersForFailover(appType);
// Mutations
const addToQueue = useAddToFailoverQueue();
@@ -122,6 +120,7 @@ export function FailoverQueueManager({
await reorderQueue.mutateAsync({ appType, providerIds });
toast.success(
t("proxy.failoverQueue.reorderSuccess", "队列顺序已更新"),
{ closeButton: true },
);
} catch (error) {
toast.error(
@@ -144,7 +143,10 @@ export function FailoverQueueManager({
providerId: selectedProviderId,
});
setSelectedProviderId("");
toast.success(t("proxy.failoverQueue.addSuccess", "已添加到故障转移队列"));
toast.success(
t("proxy.failoverQueue.addSuccess", "已添加到故障转移队列"),
{ closeButton: true },
);
} catch (error) {
toast.error(
t("proxy.failoverQueue.addFailed", "添加失败") + ": " + String(error),
@@ -158,10 +160,13 @@ export function FailoverQueueManager({
await removeFromQueue.mutateAsync({ appType, providerId });
toast.success(
t("proxy.failoverQueue.removeSuccess", "已从故障转移队列移除"),
{ closeButton: true },
);
} catch (error) {
toast.error(
t("proxy.failoverQueue.removeFailed", "移除失败") + ": " + String(error),
t("proxy.failoverQueue.removeFailed", "移除失败") +
": " +
String(error),
);
}
};
@@ -242,9 +247,7 @@ export function FailoverQueueManager({
</Select>
<Button
onClick={handleAddProvider}
disabled={
disabled || !selectedProviderId || addToQueue.isPending
}
disabled={disabled || !selectedProviderId || addToQueue.isPending}
size="icon"
variant="outline"
>
@@ -386,9 +389,7 @@ function SortableQueueItem({
{/* 启用开关 */}
<Switch
checked={item.enabled}
onCheckedChange={(checked) =>
onToggleEnabled(item.providerId, checked)
}
onCheckedChange={(checked) => onToggleEnabled(item.providerId, checked)}
disabled={disabled || isToggling}
aria-label={t("proxy.failoverQueue.toggleEnabled", "启用/禁用")}
/>
+1 -1
View File
@@ -51,7 +51,7 @@ export function ProxyPanel() {
navigator.clipboard.writeText(
`http://${status.address}:${status.port}`,
);
toast.success("地址已复制");
toast.success("地址已复制", { closeButton: true });
}}
>
+1 -1
View File
@@ -142,7 +142,7 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
try {
const available = await checkUpdate();
if (!available) {
toast.success(t("settings.upToDate"));
toast.success(t("settings.upToDate"), { closeButton: true });
}
} catch (error) {
console.error("[AboutSection] Check update failed", error);
+1 -1
View File
@@ -136,7 +136,7 @@ export function SettingsPage({
const handleRestartNow = useCallback(async () => {
setShowRestartPrompt(false);
if (import.meta.env.DEV) {
toast.success(t("settings.devModeRestartHint"));
toast.success(t("settings.devModeRestartHint"), { closeButton: true });
closeAfterSave();
return;
}
+10 -3
View File
@@ -103,7 +103,9 @@ export const SkillsPage = forwardRef<SkillsPageHandle, SkillsPageProps>(
const handleInstall = async (directory: string) => {
try {
await skillsApi.install(directory, selectedApp);
toast.success(t("skills.installSuccess", { name: directory }));
toast.success(t("skills.installSuccess", { name: directory }), {
closeButton: true,
});
await loadSkills();
} catch (error) {
const errorMessage =
@@ -132,7 +134,9 @@ export const SkillsPage = forwardRef<SkillsPageHandle, SkillsPageProps>(
const handleUninstall = async (directory: string) => {
try {
await skillsApi.uninstall(directory, selectedApp);
toast.success(t("skills.uninstallSuccess", { name: directory }));
toast.success(t("skills.uninstallSuccess", { name: directory }), {
closeButton: true,
});
await loadSkills();
} catch (error) {
const errorMessage =
@@ -180,12 +184,15 @@ export const SkillsPage = forwardRef<SkillsPageHandle, SkillsPageProps>(
name: repo.name,
count: repoSkillCount,
}),
{ closeButton: true },
);
};
const handleRemoveRepo = async (owner: string, name: string) => {
await skillsApi.removeRepo(owner, name);
toast.success(t("skills.repo.removeSuccess", { owner, name }));
toast.success(t("skills.repo.removeSuccess", { owner, name }), {
closeButton: true,
});
await Promise.all([loadRepos(), loadSkills()]);
};
@@ -47,7 +47,9 @@ export function ModelTestConfigPanel() {
try {
setIsSaving(true);
await saveStreamCheckConfig(config);
toast.success(t("streamCheck.configSaved", "健康检查配置已保存"));
toast.success(t("streamCheck.configSaved", "健康检查配置已保存"), {
closeButton: true,
});
} catch (e) {
toast.error(
t("streamCheck.configSaveFailed", "保存失败") + ": " + String(e),
@@ -76,6 +76,7 @@ export function PricingEditModal({
isNew
? t("usage.pricingAdded", "定价已添加")
: t("usage.pricingUpdated", "定价已更新"),
{ closeButton: true },
);
onClose();
+1
View File
@@ -87,6 +87,7 @@ export function useDragSort(providers: Record<string, Provider>, appId: AppId) {
t("provider.sortUpdated", {
defaultValue: "排序已更新",
}),
{ closeButton: true },
);
} catch (error) {
console.error("Failed to update provider sort order", error);
+2
View File
@@ -113,6 +113,7 @@ export function useImportExport(
t("settings.importSuccess", {
defaultValue: "配置导入成功",
}),
{ closeButton: true },
);
successTimerRef.current = window.setTimeout(() => {
@@ -170,6 +171,7 @@ export function useImportExport(
t("settings.configExported", {
defaultValue: "配置已导出",
}) + `\n${displayPath}`,
{ closeButton: true },
);
} else {
toast.error(
+6 -6
View File
@@ -36,7 +36,7 @@ export function usePromptActions(appId: AppId) {
try {
await promptsApi.upsertPrompt(appId, id, prompt);
await reload();
toast.success(t("prompts.saveSuccess"));
toast.success(t("prompts.saveSuccess"), { closeButton: true });
} catch (error) {
toast.error(t("prompts.saveFailed"));
throw error;
@@ -50,7 +50,7 @@ export function usePromptActions(appId: AppId) {
try {
await promptsApi.deletePrompt(appId, id);
await reload();
toast.success(t("prompts.deleteSuccess"));
toast.success(t("prompts.deleteSuccess"), { closeButton: true });
} catch (error) {
toast.error(t("prompts.deleteFailed"));
throw error;
@@ -64,7 +64,7 @@ export function usePromptActions(appId: AppId) {
try {
await promptsApi.enablePrompt(appId, id);
await reload();
toast.success(t("prompts.enableSuccess"));
toast.success(t("prompts.enableSuccess"), { closeButton: true });
} catch (error) {
toast.error(t("prompts.enableFailed"));
throw error;
@@ -104,14 +104,14 @@ export function usePromptActions(appId: AppId) {
try {
if (enabled) {
await promptsApi.enablePrompt(appId, id);
toast.success(t("prompts.enableSuccess"));
toast.success(t("prompts.enableSuccess"), { closeButton: true });
} else {
// 禁用提示词 - 需要后端支持
await promptsApi.upsertPrompt(appId, id, {
...prompts[id],
enabled: false,
});
toast.success(t("prompts.disableSuccess"));
toast.success(t("prompts.disableSuccess"), { closeButton: true });
}
await reload();
} catch (error) {
@@ -130,7 +130,7 @@ export function usePromptActions(appId: AppId) {
try {
const id = await promptsApi.importFromFile(appId);
await reload();
toast.success(t("prompts.importSuccess"));
toast.success(t("prompts.importSuccess"), { closeButton: true });
return id;
} catch (error) {
toast.error(t("prompts.importFailed"));
+1
View File
@@ -119,6 +119,7 @@ export function useProviderActions(activeApp: AppId) {
t("provider.usageSaved", {
defaultValue: "用量查询配置已保存",
}),
{ closeButton: true },
);
} catch (error) {
const detail =
+1 -1
View File
@@ -24,7 +24,7 @@ export function useProxyConfig() {
mutationFn: (newConfig: ProxyConfig) =>
invoke("update_proxy_config", { config: newConfig }),
onSuccess: () => {
toast.success("代理配置已保存");
toast.success("代理配置已保存", { closeButton: true });
queryClient.invalidateQueries({ queryKey: ["proxyConfig"] });
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
},
+2
View File
@@ -40,6 +40,7 @@ export function useProxyStatus() {
t("proxy.startedWithTakeover", {
defaultValue: `代理模式已启用 - ${info.address}:${info.port}`,
}),
{ closeButton: true },
);
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
queryClient.invalidateQueries({ queryKey: ["proxyTakeoverActive"] });
@@ -62,6 +63,7 @@ export function useProxyStatus() {
t("proxy.stoppedWithRestore", {
defaultValue: "代理模式已关闭,配置已恢复",
}),
{ closeButton: true },
);
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
queryClient.invalidateQueries({ queryKey: ["proxyTakeoverActive"] });
+1
View File
@@ -309,6 +309,7 @@ export function useSettings(): UseSettingsResult {
t("notifications.settingsSaved", {
defaultValue: "设置已保存",
}),
{ closeButton: true },
);
}
+1
View File
@@ -28,6 +28,7 @@ export function useStreamCheck(appId: AppId) {
time: result.responseTimeMs,
defaultValue: `${providerName} 运行正常 (${result.responseTimeMs}ms)`,
}),
{ closeButton: true },
);
} else if (result.status === "degraded") {
toast.warning(
+2 -2
View File
@@ -86,7 +86,7 @@ export const deeplinkApi = {
* @returns Merged deep link request with config fields populated
*/
mergeDeeplinkConfig: async (
request: DeepLinkImportRequest
request: DeepLinkImportRequest,
): Promise<DeepLinkImportRequest> => {
return invoke("merge_deeplink_config", { request });
},
@@ -97,7 +97,7 @@ export const deeplinkApi = {
* @returns Import result based on resource type
*/
importFromDeeplink: async (
request: DeepLinkImportRequest
request: DeepLinkImportRequest,
): Promise<ImportResult> => {
return invoke("import_from_deeplink_unified", { request });
},
+6 -5
View File
@@ -89,10 +89,7 @@ export const failoverApi = {
},
// 添加供应商到故障转移队列
async addToFailoverQueue(
appType: string,
providerId: string,
): Promise<void> {
async addToFailoverQueue(appType: string, providerId: string): Promise<void> {
return invoke("add_to_failover_queue", { appType, providerId });
},
@@ -118,6 +115,10 @@ export const failoverApi = {
providerId: string,
enabled: boolean,
): Promise<void> {
return invoke("set_failover_item_enabled", { appType, providerId, enabled });
return invoke("set_failover_item_enabled", {
appType,
providerId,
enabled,
});
},
};
+1
View File
@@ -194,6 +194,7 @@ export const useSetProxyTargetMutation = (appId: AppId) => {
t("notifications.proxyTargetSet", {
defaultValue: "已设置代理目标",
}),
{ closeButton: true },
);
},
onError: (error: Error) => {