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:
@@ -14,6 +14,7 @@
|
||||
{
|
||||
"label": "main",
|
||||
"title": "",
|
||||
"titleBarStyle": "Overlay",
|
||||
"width": 1000,
|
||||
"height": 650,
|
||||
"minWidth": 900,
|
||||
|
||||
4
src/components/env/EnvWarningBanner.tsx
vendored
4
src/components/env/EnvWarningBanner.tsx
vendored
@@ -110,7 +110,7 @@ export function EnvWarningBanner({
|
||||
|
||||
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="flex items-start gap-3">
|
||||
<AlertTriangle className="h-5 w-5 text-yellow-600 dark:text-yellow-500 flex-shrink-0 mt-0.5" />
|
||||
@@ -241,7 +241,7 @@ export function EnvWarningBanner({
|
||||
</div>
|
||||
|
||||
<Dialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogContent className="max-w-md" zIndex="top">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-5 w-5 text-destructive" />
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Plus, Server, Check } from "lucide-react";
|
||||
import { Server } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useAllMcpServers, useToggleMcpApp } from "@/hooks/useMcp";
|
||||
import type { McpServer } from "@/types";
|
||||
@@ -22,7 +15,6 @@ import { mcpPresets } from "@/config/mcpPresets";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface UnifiedMcpPanelProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
@@ -30,10 +22,13 @@ interface UnifiedMcpPanelProps {
|
||||
* 统一 MCP 管理面板
|
||||
* v3.7.0 新架构:所有 MCP 服务器统一管理,每个服务器通过复选框控制应用到哪些客户端
|
||||
*/
|
||||
const UnifiedMcpPanel: React.FC<UnifiedMcpPanelProps> = ({
|
||||
open,
|
||||
export interface UnifiedMcpPanelHandle {
|
||||
openAdd: () => void;
|
||||
}
|
||||
|
||||
const UnifiedMcpPanel = React.forwardRef<UnifiedMcpPanelHandle, UnifiedMcpPanelProps>(({
|
||||
onOpenChange,
|
||||
}) => {
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [isFormOpen, setIsFormOpen] = useState(false);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
@@ -90,6 +85,10 @@ const UnifiedMcpPanel: React.FC<UnifiedMcpPanelProps> = ({
|
||||
setIsFormOpen(true);
|
||||
};
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
openAdd: handleAdd
|
||||
}));
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
setConfirmDialog({
|
||||
isOpen: true,
|
||||
@@ -115,22 +114,10 @@ const UnifiedMcpPanel: React.FC<UnifiedMcpPanelProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-3xl max-h-[85vh] min-h-[600px] flex flex-col">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center justify-between pr-8">
|
||||
<DialogTitle>{t("mcp.unifiedPanel.title")}</DialogTitle>
|
||||
<Button type="button" variant="mcp" onClick={handleAdd}>
|
||||
<Plus size={16} />
|
||||
{t("mcp.unifiedPanel.addServer")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="mx-auto max-w-5xl flex flex-col h-[calc(100vh-8rem)] overflow-hidden">
|
||||
{/* Info Section */}
|
||||
<div className="flex-shrink-0 px-6 py-4">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="flex-shrink-0 px-6 py-4 glass rounded-xl border border-white/10 mb-4">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("mcp.serverCount", { count: serverEntries.length })} ·{" "}
|
||||
{t("mcp.unifiedPanel.apps.claude")}: {enabledCounts.claude} ·{" "}
|
||||
{t("mcp.unifiedPanel.apps.codex")}: {enabledCounts.codex} ·{" "}
|
||||
@@ -139,7 +126,7 @@ const UnifiedMcpPanel: React.FC<UnifiedMcpPanelProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Content - Scrollable */}
|
||||
<div className="flex-1 overflow-y-auto px-6 pb-4">
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden px-6 pb-24">
|
||||
{isLoading ? (
|
||||
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||
{t("mcp.loading")}
|
||||
@@ -175,19 +162,6 @@ const UnifiedMcpPanel: React.FC<UnifiedMcpPanelProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="mcp"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
<Check size={16} />
|
||||
{t("common.done")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Form Modal */}
|
||||
{isFormOpen && (
|
||||
<McpFormModal
|
||||
@@ -215,9 +189,11 @@ const UnifiedMcpPanel: React.FC<UnifiedMcpPanelProps> = ({
|
||||
onCancel={() => setConfirmDialog(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
UnifiedMcpPanel.displayName = "UnifiedMcpPanel";
|
||||
|
||||
/**
|
||||
* 统一 MCP 列表项组件
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Plus } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
|
||||
import type { Provider, CustomEndpoint } from "@/types";
|
||||
import type { AppId } from "@/lib/api";
|
||||
import {
|
||||
@@ -58,8 +51,6 @@ export function AddProviderDialog({
|
||||
|
||||
if (!hasCustomEndpoints) {
|
||||
// 收集端点候选(仅在缺少自定义端点时兜底)
|
||||
// 1. 从预设配置中获取 endpointCandidates
|
||||
// 2. 从当前配置中提取 baseUrl (ANTHROPIC_BASE_URL 或 Codex base_url)
|
||||
const urlSet = new Set<string>();
|
||||
|
||||
const addUrl = (rawUrl?: string) => {
|
||||
@@ -170,15 +161,33 @@ export function AddProviderDialog({
|
||||
? t("provider.addCodexProvider")
|
||||
: t("provider.addGeminiProvider");
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-3xl max-h-[85vh] min-h-[600px] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{submitLabel}</DialogTitle>
|
||||
<DialogDescription>{t("provider.addProviderHint")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
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>
|
||||
</>
|
||||
);
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||
return (
|
||||
<FullScreenPanel
|
||||
isOpen={open}
|
||||
title={submitLabel}
|
||||
onClose={() => onOpenChange(false)}
|
||||
footer={footer}
|
||||
>
|
||||
<ProviderForm
|
||||
appId={appId}
|
||||
submitLabel={t("common.add")}
|
||||
@@ -186,18 +195,6 @@ export function AddProviderDialog({
|
||||
onCancel={() => onOpenChange(false)}
|
||||
showButtons={false}
|
||||
/>
|
||||
</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>
|
||||
</FullScreenPanel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Save } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
|
||||
import type { Provider } from "@/types";
|
||||
import {
|
||||
ProviderForm,
|
||||
@@ -34,7 +27,7 @@ export function EditProviderDialog({
|
||||
}: EditProviderDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 默认使用传入的 provider.settingsConfig,若当前编辑对象是“当前生效供应商”,则尝试读取实时配置替换初始值
|
||||
// 默认使用传入的 provider.settingsConfig,若当前编辑对象是"当前生效供应商",则尝试读取实时配置替换初始值
|
||||
const [liveSettings, setLiveSettings] = useState<Record<
|
||||
string,
|
||||
unknown
|
||||
@@ -112,16 +105,11 @@ export function EditProviderDialog({
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-3xl max-h-[85vh] min-h-[600px] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("provider.editProvider")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("provider.editProviderHint")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||
<FullScreenPanel
|
||||
isOpen={open}
|
||||
title={t("provider.editProvider")}
|
||||
onClose={() => onOpenChange(false)}
|
||||
>
|
||||
<ProviderForm
|
||||
appId={appId}
|
||||
providerId={provider.id}
|
||||
@@ -139,18 +127,16 @@ export function EditProviderDialog({
|
||||
}}
|
||||
showButtons={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" form="provider-form">
|
||||
<Save className="h-4 w-4" />
|
||||
<div className="flex justify-end pt-6">
|
||||
<Button
|
||||
type="submit"
|
||||
form="provider-form"
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</FullScreenPanel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@ export function SkillCard({ skill, onInstall, onUninstall }: SkillCardProps) {
|
||||
skill.directory.trim().toLowerCase() !== skill.name.trim().toLowerCase();
|
||||
|
||||
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">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -95,7 +96,7 @@ export function SkillCard({ skill, onInstall, onUninstall }: SkillCardProps) {
|
||||
{skill.description || t("skills.noDescription")}
|
||||
</p>
|
||||
</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 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, forwardRef, useImperativeHandle } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RefreshCw, Settings } from "lucide-react";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { SkillCard } from "./SkillCard";
|
||||
import { RepoManager } from "./RepoManager";
|
||||
import { RepoManagerPanel } from "./RepoManagerPanel";
|
||||
import { skillsApi, type Skill, type SkillRepo } from "@/lib/api/skills";
|
||||
|
||||
interface SkillsPageProps {
|
||||
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 [skills, setSkills] = useState<Skill[]>([]);
|
||||
const [repos, setRepos] = useState<SkillRepo[]>([]);
|
||||
@@ -48,6 +53,11 @@ export function SkillsPage({ onClose: _onClose }: SkillsPageProps = {}) {
|
||||
Promise.all([loadSkills(), loadRepos()]);
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
refresh: () => loadSkills(),
|
||||
openRepoManager: () => setRepoManagerOpen(true)
|
||||
}));
|
||||
|
||||
const handleInstall = async (directory: string) => {
|
||||
try {
|
||||
await skillsApi.install(directory);
|
||||
@@ -104,44 +114,11 @@ export function SkillsPage({ onClose: _onClose }: SkillsPageProps = {}) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full min-h-0 bg-background">
|
||||
{/* 顶部操作栏(固定区域) */}
|
||||
<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 flex-col h-full min-h-0 bg-background/50">
|
||||
{/* 顶部操作栏(固定区域)已移除,由 App.tsx 接管 */}
|
||||
|
||||
{/* 技能网格(可滚动详情区域) */}
|
||||
<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 ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<RefreshCw className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
@@ -176,15 +153,18 @@ export function SkillsPage({ onClose: _onClose }: SkillsPageProps = {}) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 仓库管理对话框 */}
|
||||
<RepoManager
|
||||
open={repoManagerOpen}
|
||||
onOpenChange={setRepoManagerOpen}
|
||||
{/* 仓库管理面板 */}
|
||||
{repoManagerOpen && (
|
||||
<RepoManagerPanel
|
||||
repos={repos}
|
||||
skills={skills}
|
||||
onAdd={handleAddRepo}
|
||||
onRemove={handleRemoveRepo}
|
||||
onClose={() => setRepoManagerOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
SkillsPage.displayName = "SkillsPage";
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
"formatSuccess": "Formatted successfully",
|
||||
"formatError": "Format failed: {{error}}",
|
||||
"copy": "Copy",
|
||||
"view": "View"
|
||||
"view": "View",
|
||||
"back": "Back"
|
||||
},
|
||||
"apiKeyInput": {
|
||||
"placeholder": "Enter API Key",
|
||||
@@ -314,7 +315,8 @@
|
||||
"pleaseAddEndpoint": "Please add an endpoint first",
|
||||
"testUnavailable": "Speed test unavailable",
|
||||
"noResult": "No result returned",
|
||||
"testFailed": "Speed test failed: {{error}}"
|
||||
"testFailed": "Speed test failed: {{error}}",
|
||||
"status": "Status: {{code}}"
|
||||
},
|
||||
"codexConfig": {
|
||||
"authJson": "auth.json (JSON) *",
|
||||
@@ -361,6 +363,9 @@
|
||||
"title": "Configure Usage Query",
|
||||
"enableUsageQuery": "Enable usage query",
|
||||
"presetTemplate": "Preset template",
|
||||
"requestUrl": "Request URL",
|
||||
"requestUrlPlaceholder": "e.g. https://api.example.com",
|
||||
"method": "HTTP method",
|
||||
"templateCustom": "Custom",
|
||||
"templateGeneral": "General",
|
||||
"templateNewAPI": "NewAPI",
|
||||
@@ -373,11 +378,14 @@
|
||||
"queryFailedMessage": "Query failed",
|
||||
"queryScript": "Query script (JavaScript)",
|
||||
"timeoutSeconds": "Timeout (seconds)",
|
||||
"headers": "Headers",
|
||||
"body": "Body",
|
||||
"timeoutHint": "Range: 2-30 seconds",
|
||||
"timeoutMustBeInteger": "Timeout must be an integer, decimal part ignored",
|
||||
"timeoutCannotBeNegative": "Timeout cannot be negative",
|
||||
"autoIntervalMinutes": "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",
|
||||
"intervalCannotBeNegative": "Interval cannot be negative",
|
||||
"intervalAdjusted": "Interval adjusted to {{value}} minutes",
|
||||
@@ -398,6 +406,9 @@
|
||||
"formatSuccess": "Format successful",
|
||||
"formatFailed": "Format failed",
|
||||
"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",
|
||||
"fieldInvalidMessage": "• invalidMessage: String, reason for expiration (shown when isValid is false)",
|
||||
"fieldRemaining": "• remaining: Number, remaining quota",
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
"formatSuccess": "格式化成功",
|
||||
"formatError": "格式化失败:{{error}}",
|
||||
"copy": "复制",
|
||||
"view": "查看"
|
||||
"view": "查看",
|
||||
"back": "返回"
|
||||
},
|
||||
"apiKeyInput": {
|
||||
"placeholder": "请输入API Key",
|
||||
@@ -314,7 +315,8 @@
|
||||
"pleaseAddEndpoint": "请先添加端点",
|
||||
"testUnavailable": "测速功能不可用",
|
||||
"noResult": "未返回结果",
|
||||
"testFailed": "测速失败: {{error}}"
|
||||
"testFailed": "测速失败: {{error}}",
|
||||
"status": "状态码:{{code}}"
|
||||
},
|
||||
"codexConfig": {
|
||||
"authJson": "auth.json (JSON) *",
|
||||
@@ -361,6 +363,9 @@
|
||||
"title": "配置用量查询",
|
||||
"enableUsageQuery": "启用用量查询",
|
||||
"presetTemplate": "预设模板",
|
||||
"requestUrl": "请求地址",
|
||||
"requestUrlPlaceholder": "例如:https://api.example.com",
|
||||
"method": "HTTP 方法",
|
||||
"templateCustom": "自定义",
|
||||
"templateGeneral": "通用模板",
|
||||
"templateNewAPI": "NewAPI",
|
||||
@@ -373,11 +378,14 @@
|
||||
"queryFailedMessage": "查询失败",
|
||||
"queryScript": "查询脚本(JavaScript)",
|
||||
"timeoutSeconds": "超时时间(秒)",
|
||||
"headers": "请求头",
|
||||
"body": "请求 Body",
|
||||
"timeoutHint": "范围: 2-30 秒",
|
||||
"timeoutMustBeInteger": "超时时间必须为整数,小数部分已忽略",
|
||||
"timeoutCannotBeNegative": "超时时间不能为负数",
|
||||
"autoIntervalMinutes": "自动查询间隔(分钟)",
|
||||
"autoQueryInterval": "自动查询间隔(分钟)",
|
||||
"autoQueryIntervalHint": "0 表示不自动查询,建议设置 5-60 分钟",
|
||||
"autoQueryIntervalHint": "0 表示不自动查询,建议 5-60 分钟",
|
||||
"intervalMustBeInteger": "自动查询间隔必须为整数,小数部分已忽略",
|
||||
"intervalCannotBeNegative": "自动查询间隔不能为负数",
|
||||
"intervalAdjusted": "自动查询间隔已调整为 {{value}} 分钟",
|
||||
@@ -398,6 +406,9 @@
|
||||
"formatSuccess": "格式化成功",
|
||||
"formatFailed": "格式化失败",
|
||||
"variablesHint": "支持变量: {{apiKey}}, {{baseUrl}} | extractor 函数接收 API 响应的 JSON 对象",
|
||||
"scriptConfig": "请求配置",
|
||||
"extractorCode": "提取器代码",
|
||||
"extractorHint": "返回对象需包含剩余额度等字段",
|
||||
"fieldIsValid": "• isValid: 布尔值,套餐是否有效",
|
||||
"fieldInvalidMessage": "• invalidMessage: 字符串,失效原因说明(当 isValid 为 false 时显示)",
|
||||
"fieldRemaining": "• remaining: 数字,剩余额度",
|
||||
|
||||
@@ -169,7 +169,6 @@ export const useSwitchProviderMutation = (appId: AppId) => {
|
||||
|
||||
export const useSaveSettingsMutation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (settings: Settings) => {
|
||||
@@ -177,19 +176,6 @@ export const useSaveSettingsMutation = () => {
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["settings"] });
|
||||
toast.success(
|
||||
t("notifications.settingsSaved", {
|
||||
defaultValue: "设置已保存",
|
||||
}),
|
||||
);
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(
|
||||
t("notifications.settingsSaveFailed", {
|
||||
defaultValue: "保存设置失败: {{error}}",
|
||||
error: error.message,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user