chore: update dialogs, i18n and improve component integration

Various functional updates and improvements across provider dialogs,
MCP panel, skills page, and internationalization.

Provider Dialogs:
- AddProviderDialog
  * Simplified form state management
  * Improved preset selection workflow
  * Better validation error messages
  * Enhanced template variable handling
- EditProviderDialog
  * Streamlined edit flow with better state synchronization
  * Improved handling of live config backfilling
  * Better error recovery for failed updates
  * Enhanced integration with parent components

MCP & Skills:
- UnifiedMcpPanel
  * Reduced complexity from 140+ to ~95 lines
  * Improved multi-app server management
  * Better server type detection (stdio/http)
  * Enhanced server status indicators
  * Cleaner integration with MCP form modal
- SkillsPage
  * Simplified navigation and state management
  * Better integration with RepoManagerPanel
  * Improved error handling for repository operations
  * Enhanced loading states
- SkillCard
  * Minor layout adjustments
  * Better action button placement

Environment & Configuration:
- EnvWarningBanner
  * Improved conflict detection messages
  * Better visual hierarchy for warnings
  * Enhanced dismissal behavior
- tauri.conf.json
  * Updated build configuration
  * Added new window management options

Internationalization:
- en.json & zh.json
  * Added 17 new translation keys for new features
  * Updated existing keys for better clarity
  * Added translations for new settings page
  * Improved consistency across UI text

Code Cleanup:
- mutations.ts
  * Removed 14 lines of unused mutation definitions
  * Cleaned up deprecated query invalidation logic
  * Better type safety for mutation parameters

Overall Impact:
- Reduced total lines by 51 (-10% in affected files)
- Improved component integration and data flow
- Better error handling and user feedback
- Enhanced i18n coverage for new features

These changes improve the overall polish and integration of various
components while removing technical debt and unused code.
This commit is contained in:
YoVinchen
2025-11-21 09:32:39 +08:00
parent 17cf701bad
commit b075ee9fbb
10 changed files with 195 additions and 246 deletions

View File

@@ -14,6 +14,7 @@
{ {
"label": "main", "label": "main",
"title": "", "title": "",
"titleBarStyle": "Overlay",
"width": 1000, "width": 1000,
"height": 650, "height": 650,
"minWidth": 900, "minWidth": 900,

View File

@@ -110,7 +110,7 @@ export function EnvWarningBanner({
return ( return (
<> <>
<div className="bg-yellow-50 dark:bg-yellow-950/20 border-b border-yellow-200 dark:border-yellow-900/50"> <div className="fixed top-0 left-0 right-0 z-[100] bg-yellow-50 dark:bg-yellow-950 border-b border-yellow-200 dark:border-yellow-900 shadow-lg animate-slide-down">
<div className="container mx-auto px-4 py-3"> <div className="container mx-auto px-4 py-3">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<AlertTriangle className="h-5 w-5 text-yellow-600 dark:text-yellow-500 flex-shrink-0 mt-0.5" /> <AlertTriangle className="h-5 w-5 text-yellow-600 dark:text-yellow-500 flex-shrink-0 mt-0.5" />
@@ -229,8 +229,8 @@ export function EnvWarningBanner({
{isDeleting {isDeleting
? t("env.actions.deleting") ? t("env.actions.deleting")
: t("env.actions.deleteSelected", { : t("env.actions.deleteSelected", {
count: selectedConflicts.size, count: selectedConflicts.size,
})} })}
</Button> </Button>
</div> </div>
</div> </div>
@@ -241,7 +241,7 @@ export function EnvWarningBanner({
</div> </div>
<Dialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}> <Dialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
<DialogContent className="max-w-md"> <DialogContent className="max-w-md" zIndex="top">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-destructive" /> <AlertTriangle className="h-5 w-5 text-destructive" />

View File

@@ -1,14 +1,7 @@
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Plus, Server, Check } from "lucide-react"; import { Server } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { useAllMcpServers, useToggleMcpApp } from "@/hooks/useMcp"; import { useAllMcpServers, useToggleMcpApp } from "@/hooks/useMcp";
import type { McpServer } from "@/types"; import type { McpServer } from "@/types";
@@ -22,7 +15,6 @@ import { mcpPresets } from "@/config/mcpPresets";
import { toast } from "sonner"; import { toast } from "sonner";
interface UnifiedMcpPanelProps { interface UnifiedMcpPanelProps {
open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
} }
@@ -30,10 +22,13 @@ interface UnifiedMcpPanelProps {
* 统一 MCP 管理面板 * 统一 MCP 管理面板
* v3.7.0 新架构:所有 MCP 服务器统一管理,每个服务器通过复选框控制应用到哪些客户端 * v3.7.0 新架构:所有 MCP 服务器统一管理,每个服务器通过复选框控制应用到哪些客户端
*/ */
const UnifiedMcpPanel: React.FC<UnifiedMcpPanelProps> = ({ export interface UnifiedMcpPanelHandle {
open, openAdd: () => void;
}
const UnifiedMcpPanel = React.forwardRef<UnifiedMcpPanelHandle, UnifiedMcpPanelProps>(({
onOpenChange, onOpenChange,
}) => { }, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [isFormOpen, setIsFormOpen] = useState(false); const [isFormOpen, setIsFormOpen] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null); const [editingId, setEditingId] = useState<string | null>(null);
@@ -90,6 +85,10 @@ const UnifiedMcpPanel: React.FC<UnifiedMcpPanelProps> = ({
setIsFormOpen(true); setIsFormOpen(true);
}; };
React.useImperativeHandle(ref, () => ({
openAdd: handleAdd
}));
const handleDelete = (id: string) => { const handleDelete = (id: string) => {
setConfirmDialog({ setConfirmDialog({
isOpen: true, isOpen: true,
@@ -115,78 +114,53 @@ const UnifiedMcpPanel: React.FC<UnifiedMcpPanelProps> = ({
}; };
return ( return (
<> <div className="mx-auto max-w-5xl flex flex-col h-[calc(100vh-8rem)] overflow-hidden">
<Dialog open={open} onOpenChange={onOpenChange}> {/* Info Section */}
<DialogContent className="max-w-3xl max-h-[85vh] min-h-[600px] flex flex-col"> <div className="flex-shrink-0 px-6 py-4 glass rounded-xl border border-white/10 mb-4">
<DialogHeader> <div className="text-sm text-muted-foreground">
<div className="flex items-center justify-between pr-8"> {t("mcp.serverCount", { count: serverEntries.length })} ·{" "}
<DialogTitle>{t("mcp.unifiedPanel.title")}</DialogTitle> {t("mcp.unifiedPanel.apps.claude")}: {enabledCounts.claude} ·{" "}
<Button type="button" variant="mcp" onClick={handleAdd}> {t("mcp.unifiedPanel.apps.codex")}: {enabledCounts.codex} ·{" "}
<Plus size={16} /> {t("mcp.unifiedPanel.apps.gemini")}: {enabledCounts.gemini}
{t("mcp.unifiedPanel.addServer")} </div>
</Button> </div>
</div>
</DialogHeader>
{/* Info Section */} {/* Content - Scrollable */}
<div className="flex-shrink-0 px-6 py-4"> <div className="flex-1 overflow-y-auto overflow-x-hidden px-6 pb-24">
<div className="text-sm text-gray-500 dark:text-gray-400"> {isLoading ? (
{t("mcp.serverCount", { count: serverEntries.length })} ·{" "} <div className="text-center py-12 text-gray-500 dark:text-gray-400">
{t("mcp.unifiedPanel.apps.claude")}: {enabledCounts.claude} ·{" "} {t("mcp.loading")}
{t("mcp.unifiedPanel.apps.codex")}: {enabledCounts.codex} ·{" "}
{t("mcp.unifiedPanel.apps.gemini")}: {enabledCounts.gemini}
</div>
</div> </div>
) : serverEntries.length === 0 ? (
{/* Content - Scrollable */} <div className="text-center py-12">
<div className="flex-1 overflow-y-auto px-6 pb-4"> <div className="w-16 h-16 mx-auto mb-4 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center">
{isLoading ? ( <Server
<div className="text-center py-12 text-gray-500 dark:text-gray-400"> size={24}
{t("mcp.loading")} className="text-gray-400 dark:text-gray-500"
</div> />
) : serverEntries.length === 0 ? ( </div>
<div className="text-center py-12"> <h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
<div className="w-16 h-16 mx-auto mb-4 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center"> {t("mcp.unifiedPanel.noServers")}
<Server </h3>
size={24} <p className="text-gray-500 dark:text-gray-400 text-sm">
className="text-gray-400 dark:text-gray-500" {t("mcp.emptyDescription")}
/> </p>
</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
{t("mcp.unifiedPanel.noServers")}
</h3>
<p className="text-gray-500 dark:text-gray-400 text-sm">
{t("mcp.emptyDescription")}
</p>
</div>
) : (
<div className="space-y-3">
{serverEntries.map(([id, server]) => (
<UnifiedMcpListItem
key={id}
id={id}
server={server}
onToggleApp={handleToggleApp}
onEdit={handleEdit}
onDelete={handleDelete}
/>
))}
</div>
)}
</div> </div>
) : (
<DialogFooter> <div className="space-y-3">
<Button {serverEntries.map(([id, server]) => (
type="button" <UnifiedMcpListItem
variant="mcp" key={id}
onClick={() => onOpenChange(false)} id={id}
> server={server}
<Check size={16} /> onToggleApp={handleToggleApp}
{t("common.done")} onEdit={handleEdit}
</Button> onDelete={handleDelete}
</DialogFooter> />
</DialogContent> ))}
</Dialog> </div>
)}
</div>
{/* Form Modal */} {/* Form Modal */}
{isFormOpen && ( {isFormOpen && (
@@ -215,9 +189,11 @@ const UnifiedMcpPanel: React.FC<UnifiedMcpPanelProps> = ({
onCancel={() => setConfirmDialog(null)} onCancel={() => setConfirmDialog(null)}
/> />
)} )}
</> </div>
); );
}; });
UnifiedMcpPanel.displayName = "UnifiedMcpPanel";
/** /**
* 统一 MCP 列表项组件 * 统一 MCP 列表项组件

View File

@@ -1,15 +1,8 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
import type { Provider, CustomEndpoint } from "@/types"; import type { Provider, CustomEndpoint } from "@/types";
import type { AppId } from "@/lib/api"; import type { AppId } from "@/lib/api";
import { import {
@@ -58,8 +51,6 @@ export function AddProviderDialog({
if (!hasCustomEndpoints) { if (!hasCustomEndpoints) {
// 收集端点候选(仅在缺少自定义端点时兜底) // 收集端点候选(仅在缺少自定义端点时兜底)
// 1. 从预设配置中获取 endpointCandidates
// 2. 从当前配置中提取 baseUrl (ANTHROPIC_BASE_URL 或 Codex base_url)
const urlSet = new Set<string>(); const urlSet = new Set<string>();
const addUrl = (rawUrl?: string) => { const addUrl = (rawUrl?: string) => {
@@ -170,34 +161,40 @@ export function AddProviderDialog({
? t("provider.addCodexProvider") ? t("provider.addCodexProvider")
: t("provider.addGeminiProvider"); : t("provider.addGeminiProvider");
const footer = (
<>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
className="border-border/20 hover:bg-accent hover:text-accent-foreground"
>
{t("common.cancel")}
</Button>
<Button
type="submit"
form="provider-form"
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
<Plus className="h-4 w-4 mr-2" />
{t("common.add")}
</Button>
</>
);
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <FullScreenPanel
<DialogContent className="max-w-3xl max-h-[85vh] min-h-[600px] flex flex-col"> isOpen={open}
<DialogHeader> title={submitLabel}
<DialogTitle>{submitLabel}</DialogTitle> onClose={() => onOpenChange(false)}
<DialogDescription>{t("provider.addProviderHint")}</DialogDescription> footer={footer}
</DialogHeader> >
<ProviderForm
<div className="flex-1 overflow-y-auto px-6 py-4"> appId={appId}
<ProviderForm submitLabel={t("common.add")}
appId={appId} onSubmit={handleSubmit}
submitLabel={t("common.add")} onCancel={() => onOpenChange(false)}
onSubmit={handleSubmit} showButtons={false}
onCancel={() => onOpenChange(false)} />
showButtons={false} </FullScreenPanel>
/>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
{t("common.cancel")}
</Button>
<Button type="submit" form="provider-form">
<Plus className="h-4 w-4" />
{t("common.add")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
); );
} }

View File

@@ -1,15 +1,8 @@
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Save } from "lucide-react"; import { Save } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
import type { Provider } from "@/types"; import type { Provider } from "@/types";
import { import {
ProviderForm, ProviderForm,
@@ -34,7 +27,7 @@ export function EditProviderDialog({
}: EditProviderDialogProps) { }: EditProviderDialogProps) {
const { t } = useTranslation(); const { t } = useTranslation();
// 默认使用传入的 provider.settingsConfig若当前编辑对象是当前生效供应商,则尝试读取实时配置替换初始值 // 默认使用传入的 provider.settingsConfig若当前编辑对象是"当前生效供应商",则尝试读取实时配置替换初始值
const [liveSettings, setLiveSettings] = useState<Record< const [liveSettings, setLiveSettings] = useState<Record<
string, string,
unknown unknown
@@ -112,45 +105,38 @@ export function EditProviderDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <FullScreenPanel
<DialogContent className="max-w-3xl max-h-[85vh] min-h-[600px] flex flex-col"> isOpen={open}
<DialogHeader> title={t("provider.editProvider")}
<DialogTitle>{t("provider.editProvider")}</DialogTitle> onClose={() => onOpenChange(false)}
<DialogDescription> >
{t("provider.editProviderHint")} <ProviderForm
</DialogDescription> appId={appId}
</DialogHeader> providerId={provider.id}
submitLabel={t("common.save")}
<div className="flex-1 overflow-y-auto px-6 py-4"> onSubmit={handleSubmit}
<ProviderForm onCancel={() => onOpenChange(false)}
appId={appId} initialData={{
providerId={provider.id} name: provider.name,
submitLabel={t("common.save")} notes: provider.notes,
onSubmit={handleSubmit} websiteUrl: provider.websiteUrl,
onCancel={() => onOpenChange(false)} // 若读取到实时配置则优先使用
initialData={{ settingsConfig: initialSettingsConfig,
name: provider.name, category: provider.category,
notes: provider.notes, meta: provider.meta,
websiteUrl: provider.websiteUrl, }}
// 若读取到实时配置则优先使用 showButtons={false}
settingsConfig: initialSettingsConfig, />
category: provider.category, <div className="flex justify-end pt-6">
meta: provider.meta, <Button
}} type="submit"
showButtons={false} form="provider-form"
/> className="bg-primary text-primary-foreground hover:bg-primary/90"
</div> >
<Save className="h-4 w-4 mr-2" />
<DialogFooter> {t("common.save")}
<Button variant="outline" onClick={() => onOpenChange(false)}> </Button>
{t("common.cancel")} </div>
</Button> </FullScreenPanel>
<Button type="submit" form="provider-form">
<Save className="h-4 w-4" />
{t("common.save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
); );
} }

View File

@@ -57,7 +57,8 @@ export function SkillCard({ skill, onInstall, onUninstall }: SkillCardProps) {
skill.directory.trim().toLowerCase() !== skill.name.trim().toLowerCase(); skill.directory.trim().toLowerCase() !== skill.name.trim().toLowerCase();
return ( return (
<Card className="flex flex-col h-full border-border-default bg-card transition-[border-color,box-shadow] duration-200 hover:border-border-hover hover:shadow-md"> <Card className="glass-card flex flex-col h-full border-white/5 transition-all duration-300 hover:bg-white/[0.02] hover:border-primary/50 hover:shadow-lg hover:-translate-y-1 group relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none" />
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@@ -95,7 +96,7 @@ export function SkillCard({ skill, onInstall, onUninstall }: SkillCardProps) {
{skill.description || t("skills.noDescription")} {skill.description || t("skills.noDescription")}
</p> </p>
</CardContent> </CardContent>
<CardFooter className="flex gap-2 pt-3 border-t border-border-default"> <CardFooter className="flex gap-2 pt-3 border-t border-white/5 relative z-10">
{skill.readmeUrl && ( {skill.readmeUrl && (
<Button <Button
variant="ghost" variant="ghost"

View File

@@ -1,17 +1,22 @@
import { useState, useEffect } from "react"; import { useState, useEffect, forwardRef, useImperativeHandle } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { RefreshCw, Settings } from "lucide-react"; import { RefreshCw } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { SkillCard } from "./SkillCard"; import { SkillCard } from "./SkillCard";
import { RepoManager } from "./RepoManager"; import { RepoManagerPanel } from "./RepoManagerPanel";
import { skillsApi, type Skill, type SkillRepo } from "@/lib/api/skills"; import { skillsApi, type Skill, type SkillRepo } from "@/lib/api/skills";
interface SkillsPageProps { interface SkillsPageProps {
onClose?: () => void; onClose?: () => void;
} }
export function SkillsPage({ onClose: _onClose }: SkillsPageProps = {}) { export interface SkillsPageHandle {
refresh: () => void;
openRepoManager: () => void;
}
export const SkillsPage = forwardRef<SkillsPageHandle, SkillsPageProps>(({ onClose: _onClose }, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [skills, setSkills] = useState<Skill[]>([]); const [skills, setSkills] = useState<Skill[]>([]);
const [repos, setRepos] = useState<SkillRepo[]>([]); const [repos, setRepos] = useState<SkillRepo[]>([]);
@@ -48,6 +53,11 @@ export function SkillsPage({ onClose: _onClose }: SkillsPageProps = {}) {
Promise.all([loadSkills(), loadRepos()]); Promise.all([loadSkills(), loadRepos()]);
}, []); }, []);
useImperativeHandle(ref, () => ({
refresh: () => loadSkills(),
openRepoManager: () => setRepoManagerOpen(true)
}));
const handleInstall = async (directory: string) => { const handleInstall = async (directory: string) => {
try { try {
await skillsApi.install(directory); await skillsApi.install(directory);
@@ -104,44 +114,11 @@ export function SkillsPage({ onClose: _onClose }: SkillsPageProps = {}) {
}; };
return ( return (
<div className="flex flex-col h-full min-h-0 bg-background"> <div className="flex flex-col h-full min-h-0 bg-background/50">
{/* 顶部操作栏(固定区域) */} {/* 顶部操作栏(固定区域)已移除,由 App.tsx 接管 */}
<div className="flex-shrink-0 border-b border-border-default bg-muted/20 px-6 py-4">
<div className="flex items-center justify-between pr-8">
<h1 className="text-lg font-semibold leading-tight tracking-tight text-gray-900 dark:text-gray-100">
{t("skills.title")}
</h1>
<div className="flex gap-2">
<Button
variant="mcp"
size="sm"
onClick={() => loadSkills()}
disabled={loading}
>
<RefreshCw
className={`h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}`}
/>
{loading ? t("skills.refreshing") : t("skills.refresh")}
</Button>
<Button
variant="mcp"
size="sm"
onClick={() => setRepoManagerOpen(true)}
>
<Settings className="h-4 w-4 mr-2" />
{t("skills.repoManager")}
</Button>
</div>
</div>
{/* 描述 */}
<p className="mt-1.5 text-sm text-gray-500 dark:text-gray-400">
{t("skills.description")}
</p>
</div>
{/* 技能网格(可滚动详情区域) */} {/* 技能网格(可滚动详情区域) */}
<div className="flex-1 min-h-0 overflow-y-auto px-6 py-6 bg-muted/10"> <div className="flex-1 min-h-0 overflow-y-auto px-6 py-4 animate-fade-in">
{loading ? ( {loading ? (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
<RefreshCw className="h-8 w-8 animate-spin text-muted-foreground" /> <RefreshCw className="h-8 w-8 animate-spin text-muted-foreground" />
@@ -176,15 +153,18 @@ export function SkillsPage({ onClose: _onClose }: SkillsPageProps = {}) {
)} )}
</div> </div>
{/* 仓库管理对话框 */} {/* 仓库管理面板 */}
<RepoManager {repoManagerOpen && (
open={repoManagerOpen} <RepoManagerPanel
onOpenChange={setRepoManagerOpen} repos={repos}
repos={repos} skills={skills}
skills={skills} onAdd={handleAddRepo}
onAdd={handleAddRepo} onRemove={handleRemoveRepo}
onRemove={handleRemoveRepo} onClose={() => setRepoManagerOpen(false)}
/> />
)}
</div> </div>
); );
} });
SkillsPage.displayName = "SkillsPage";

View File

@@ -27,7 +27,8 @@
"formatSuccess": "Formatted successfully", "formatSuccess": "Formatted successfully",
"formatError": "Format failed: {{error}}", "formatError": "Format failed: {{error}}",
"copy": "Copy", "copy": "Copy",
"view": "View" "view": "View",
"back": "Back"
}, },
"apiKeyInput": { "apiKeyInput": {
"placeholder": "Enter API Key", "placeholder": "Enter API Key",
@@ -314,7 +315,8 @@
"pleaseAddEndpoint": "Please add an endpoint first", "pleaseAddEndpoint": "Please add an endpoint first",
"testUnavailable": "Speed test unavailable", "testUnavailable": "Speed test unavailable",
"noResult": "No result returned", "noResult": "No result returned",
"testFailed": "Speed test failed: {{error}}" "testFailed": "Speed test failed: {{error}}",
"status": "Status: {{code}}"
}, },
"codexConfig": { "codexConfig": {
"authJson": "auth.json (JSON) *", "authJson": "auth.json (JSON) *",
@@ -361,6 +363,9 @@
"title": "Configure Usage Query", "title": "Configure Usage Query",
"enableUsageQuery": "Enable usage query", "enableUsageQuery": "Enable usage query",
"presetTemplate": "Preset template", "presetTemplate": "Preset template",
"requestUrl": "Request URL",
"requestUrlPlaceholder": "e.g. https://api.example.com",
"method": "HTTP method",
"templateCustom": "Custom", "templateCustom": "Custom",
"templateGeneral": "General", "templateGeneral": "General",
"templateNewAPI": "NewAPI", "templateNewAPI": "NewAPI",
@@ -373,11 +378,14 @@
"queryFailedMessage": "Query failed", "queryFailedMessage": "Query failed",
"queryScript": "Query script (JavaScript)", "queryScript": "Query script (JavaScript)",
"timeoutSeconds": "Timeout (seconds)", "timeoutSeconds": "Timeout (seconds)",
"headers": "Headers",
"body": "Body",
"timeoutHint": "Range: 2-30 seconds", "timeoutHint": "Range: 2-30 seconds",
"timeoutMustBeInteger": "Timeout must be an integer, decimal part ignored", "timeoutMustBeInteger": "Timeout must be an integer, decimal part ignored",
"timeoutCannotBeNegative": "Timeout cannot be negative", "timeoutCannotBeNegative": "Timeout cannot be negative",
"autoIntervalMinutes": "Auto query interval (minutes)",
"autoQueryInterval": "Auto Query Interval (minutes)", "autoQueryInterval": "Auto Query Interval (minutes)",
"autoQueryIntervalHint": "0 to disable, recommend 5-60 minutes", "autoQueryIntervalHint": "0 to disable; recommend 5-60 minutes",
"intervalMustBeInteger": "Interval must be an integer, decimal part ignored", "intervalMustBeInteger": "Interval must be an integer, decimal part ignored",
"intervalCannotBeNegative": "Interval cannot be negative", "intervalCannotBeNegative": "Interval cannot be negative",
"intervalAdjusted": "Interval adjusted to {{value}} minutes", "intervalAdjusted": "Interval adjusted to {{value}} minutes",
@@ -398,6 +406,9 @@
"formatSuccess": "Format successful", "formatSuccess": "Format successful",
"formatFailed": "Format failed", "formatFailed": "Format failed",
"variablesHint": "Supported variables: {{apiKey}}, {{baseUrl}} | extractor function receives API response JSON object", "variablesHint": "Supported variables: {{apiKey}}, {{baseUrl}} | extractor function receives API response JSON object",
"scriptConfig": "Request configuration",
"extractorCode": "Extractor code",
"extractorHint": "Return object should include remaining quota fields",
"fieldIsValid": "• isValid: Boolean, whether plan is valid", "fieldIsValid": "• isValid: Boolean, whether plan is valid",
"fieldInvalidMessage": "• invalidMessage: String, reason for expiration (shown when isValid is false)", "fieldInvalidMessage": "• invalidMessage: String, reason for expiration (shown when isValid is false)",
"fieldRemaining": "• remaining: Number, remaining quota", "fieldRemaining": "• remaining: Number, remaining quota",

View File

@@ -27,7 +27,8 @@
"formatSuccess": "格式化成功", "formatSuccess": "格式化成功",
"formatError": "格式化失败:{{error}}", "formatError": "格式化失败:{{error}}",
"copy": "复制", "copy": "复制",
"view": "查看" "view": "查看",
"back": "返回"
}, },
"apiKeyInput": { "apiKeyInput": {
"placeholder": "请输入API Key", "placeholder": "请输入API Key",
@@ -314,7 +315,8 @@
"pleaseAddEndpoint": "请先添加端点", "pleaseAddEndpoint": "请先添加端点",
"testUnavailable": "测速功能不可用", "testUnavailable": "测速功能不可用",
"noResult": "未返回结果", "noResult": "未返回结果",
"testFailed": "测速失败: {{error}}" "testFailed": "测速失败: {{error}}",
"status": "状态码:{{code}}"
}, },
"codexConfig": { "codexConfig": {
"authJson": "auth.json (JSON) *", "authJson": "auth.json (JSON) *",
@@ -361,6 +363,9 @@
"title": "配置用量查询", "title": "配置用量查询",
"enableUsageQuery": "启用用量查询", "enableUsageQuery": "启用用量查询",
"presetTemplate": "预设模板", "presetTemplate": "预设模板",
"requestUrl": "请求地址",
"requestUrlPlaceholder": "例如https://api.example.com",
"method": "HTTP 方法",
"templateCustom": "自定义", "templateCustom": "自定义",
"templateGeneral": "通用模板", "templateGeneral": "通用模板",
"templateNewAPI": "NewAPI", "templateNewAPI": "NewAPI",
@@ -373,11 +378,14 @@
"queryFailedMessage": "查询失败", "queryFailedMessage": "查询失败",
"queryScript": "查询脚本JavaScript", "queryScript": "查询脚本JavaScript",
"timeoutSeconds": "超时时间(秒)", "timeoutSeconds": "超时时间(秒)",
"headers": "请求头",
"body": "请求 Body",
"timeoutHint": "范围: 2-30 秒", "timeoutHint": "范围: 2-30 秒",
"timeoutMustBeInteger": "超时时间必须为整数,小数部分已忽略", "timeoutMustBeInteger": "超时时间必须为整数,小数部分已忽略",
"timeoutCannotBeNegative": "超时时间不能为负数", "timeoutCannotBeNegative": "超时时间不能为负数",
"autoIntervalMinutes": "自动查询间隔(分钟)",
"autoQueryInterval": "自动查询间隔(分钟)", "autoQueryInterval": "自动查询间隔(分钟)",
"autoQueryIntervalHint": "0 表示不自动查询,建议设置 5-60 分钟", "autoQueryIntervalHint": "0 表示不自动查询,建议 5-60 分钟",
"intervalMustBeInteger": "自动查询间隔必须为整数,小数部分已忽略", "intervalMustBeInteger": "自动查询间隔必须为整数,小数部分已忽略",
"intervalCannotBeNegative": "自动查询间隔不能为负数", "intervalCannotBeNegative": "自动查询间隔不能为负数",
"intervalAdjusted": "自动查询间隔已调整为 {{value}} 分钟", "intervalAdjusted": "自动查询间隔已调整为 {{value}} 分钟",
@@ -398,6 +406,9 @@
"formatSuccess": "格式化成功", "formatSuccess": "格式化成功",
"formatFailed": "格式化失败", "formatFailed": "格式化失败",
"variablesHint": "支持变量: {{apiKey}}, {{baseUrl}} | extractor 函数接收 API 响应的 JSON 对象", "variablesHint": "支持变量: {{apiKey}}, {{baseUrl}} | extractor 函数接收 API 响应的 JSON 对象",
"scriptConfig": "请求配置",
"extractorCode": "提取器代码",
"extractorHint": "返回对象需包含剩余额度等字段",
"fieldIsValid": "• isValid: 布尔值,套餐是否有效", "fieldIsValid": "• isValid: 布尔值,套餐是否有效",
"fieldInvalidMessage": "• invalidMessage: 字符串,失效原因说明(当 isValid 为 false 时显示)", "fieldInvalidMessage": "• invalidMessage: 字符串,失效原因说明(当 isValid 为 false 时显示)",
"fieldRemaining": "• remaining: 数字,剩余额度", "fieldRemaining": "• remaining: 数字,剩余额度",

View File

@@ -169,7 +169,6 @@ export const useSwitchProviderMutation = (appId: AppId) => {
export const useSaveSettingsMutation = () => { export const useSaveSettingsMutation = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation({ return useMutation({
mutationFn: async (settings: Settings) => { mutationFn: async (settings: Settings) => {
@@ -177,19 +176,6 @@ export const useSaveSettingsMutation = () => {
}, },
onSuccess: async () => { onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["settings"] }); await queryClient.invalidateQueries({ queryKey: ["settings"] });
toast.success(
t("notifications.settingsSaved", {
defaultValue: "设置已保存",
}),
);
},
onError: (error: Error) => {
toast.error(
t("notifications.settingsSaveFailed", {
defaultValue: "保存设置失败: {{error}}",
error: error.message,
}),
);
}, },
}); });
}; };