From 17cf701bade2dbf5fd0d513a9f2db88cee3bb59f Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Fri, 21 Nov 2025 09:31:36 +0800 Subject: [PATCH] style(ui): modernize component layouts and visual design Update UI components with improved layouts, visual hierarchy, and modern design patterns for better user experience. Navigation & Brand Components: - AppSwitcher * Enhanced visual design with better spacing * Improved active state indicators * Smoother transitions and hover effects * Better mobile responsiveness - BrandIcons * Optimized icon rendering performance * Added support for more provider icons * Improved SVG handling and fallbacks * Better scaling across different screen sizes Editor Components: - JsonEditor * Enhanced syntax highlighting * Better error visualization * Improved code formatting options * Added line numbers and code folding support - UsageScriptModal * Complete layout overhaul (1239 lines refactored) * Better script editor integration * Improved template selection UI * Enhanced preview and testing panels * Better error feedback and validation Provider Components: - ProviderCard * Redesigned card layout with modern aesthetics * Better information density and readability * Improved action buttons placement * Enhanced status indicators (active/inactive) - ProviderList * Better grid/list view layouts * Improved drag-and-drop visual feedback * Enhanced sorting indicators - ProviderActions * Streamlined action menu * Better icon consistency * Improved tooltips and accessibility Usage & Footer: - UsageFooter * Redesigned footer layout * Better quota visualization * Improved refresh controls * Enhanced error states Design System Updates: - dialog.tsx (shadcn/ui component) * Updated to latest design tokens * Better overlay animations * Improved focus management - index.css * Added 65 lines of global utility classes * New animation keyframes * Enhanced color variables for dark mode * Improved typography scale - tailwind.config.js * Extended theme with new design tokens * Added custom animations and transitions * New spacing and sizing utilities * Enhanced color palette Visual Improvements: - Consistent border radius across components - Unified shadow system for depth perception - Better color contrast for accessibility (WCAG AA) - Smoother animations and transitions - Improved dark mode support These changes create a more polished, modern interface while maintaining consistency with the application's design language. --- src/components/AppSwitcher.tsx | 55 +- src/components/BrandIcons.tsx | 87 +- src/components/JsonEditor.tsx | 46 +- src/components/UsageFooter.tsx | 122 +- src/components/UsageScriptModal.tsx | 1235 +++++++++--------- src/components/providers/ProviderActions.tsx | 13 +- src/components/providers/ProviderCard.tsx | 100 +- src/components/providers/ProviderList.tsx | 8 +- src/components/ui/dialog.tsx | 21 +- src/index.css | 65 + tailwind.config.js | 67 +- 11 files changed, 1006 insertions(+), 813 deletions(-) diff --git a/src/components/AppSwitcher.tsx b/src/components/AppSwitcher.tsx index f7ac954..10ec9df 100644 --- a/src/components/AppSwitcher.tsx +++ b/src/components/AppSwitcher.tsx @@ -13,22 +13,28 @@ export function AppSwitcher({ activeApp, onSwitch }: AppSwitcherProps) { }; return ( -
+
-
- - {/* 第二行:已用 + 剩余 + 单位 */} -
- {/* 已用 */} - {firstUsage.used !== undefined && ( -
- - {t("usage.used")} - - - {firstUsage.used.toFixed(2)} - -
- )} - - {/* 剩余 */} - {firstUsage.remaining !== undefined && ( -
- - {t("usage.remaining")} - - - {firstUsage.remaining.toFixed(2)} - -
- )} - - {/* 单位 */} - {firstUsage.unit && ( - - {firstUsage.unit} + {/* 剩余 */} + {firstUsage.remaining !== undefined && ( + + {t("usage.remaining")} + + {firstUsage.remaining.toFixed(2)} - )} -
+ + )} + + {/* 单位 */} + {firstUsage.unit && ( + {firstUsage.unit} + )} + + {/* 刷新按钮 */} +
); } return ( -
+
{/* 标题行:包含刷新按钮和自动查询时间 */}
@@ -196,7 +184,7 @@ const UsageFooter: React.FC = ({ +
- - setScript({ ...script, enabled: checked }) - } - aria-label={t("usageScript.enableUsageQuery")} - /> -
- {script.enabled && ( - <> - {/* 预设模板选择 */} -
- -
- {Object.keys(PRESET_TEMPLATES).map((name) => { - const isSelected = selectedTemplate === name; - return ( - - ); - })} +
+ + +
+ + ); + + return ( + +
+
+

+ {t("usageScript.enableUsageQuery")} +

+

+ {t("usageScript.autoQueryIntervalHint")} +

-
- - {/* 凭证配置区域:通用和 NewAPI 模板显示 */} - {shouldShowCredentialsConfig && ( -
-

- {t("usageScript.credentialsConfig")} -

- - {/* 通用模板:显示 apiKey + baseUrl */} - {selectedTemplate === TEMPLATE_KEYS.GENERAL && ( - <> -
- -
- - setScript({ ...script, apiKey: e.target.value }) - } - placeholder="sk-xxxxx" - autoComplete="off" - /> - {script.apiKey && ( - - )} -
-
- -
- - - setScript({ ...script, baseUrl: e.target.value }) - } - placeholder="https://api.example.com" - autoComplete="off" - /> -
- - )} - - {/* NewAPI 模板:显示 baseUrl + accessToken + userId */} - {selectedTemplate === TEMPLATE_KEYS.NEW_API && ( - <> -
- - - setScript({ ...script, baseUrl: e.target.value }) - } - placeholder="https://api.newapi.com" - autoComplete="off" - /> -
- -
- -
- - setScript({ - ...script, - accessToken: e.target.value, - }) - } - placeholder={t( - "usageScript.accessTokenPlaceholder", - )} - autoComplete="off" - /> - {script.accessToken && ( - - )} -
-
- -
- - - setScript({ ...script, userId: e.target.value }) - } - placeholder={t("usageScript.userIdPlaceholder")} - autoComplete="off" - /> -
- - )} -
- )} - - {/* 脚本编辑器 */} -
- - setScript({ ...script, code })} - height="300px" - language="javascript" + setScript({ ...script, enabled: checked })} + aria-label={t("usageScript.enableUsageQuery")} /> -

- {t("usageScript.variablesHint", { - apiKey: "{{apiKey}}", - baseUrl: "{{baseUrl}}", - })} -

-
+
- {/* 配置选项 */} -
-
- - { - // 输入时:只清理格式,允许临时为空,避免强制回填默认值 - const cleaned = sanitizeNumberInput(e.target.value); - setScript((prev) => ({ - ...prev, - timeout: - cleaned === "" ? undefined : parseInt(cleaned, 10), - })); - }} - onBlur={(e) => { - // 失焦时:严格验证并约束范围 - const validated = validateTimeout(e.target.value); - setScript({ ...script, timeout: validated }); - }} - /> -

- {t("usageScript.timeoutHint") || "范围: 2-30 秒"} -

-
+ {script.enabled && ( +
+ {/* 预设模板选择 */} +
+
+ + + {t("usageScript.variablesHint")} + +
+
+ {Object.keys(PRESET_TEMPLATES).map((name) => { + const isSelected = selectedTemplate === name; + return ( + + ); + })} +
- {/* 🆕 自动查询间隔 */} -
- - { - // 输入时:只清理格式,允许临时为空 - const cleaned = sanitizeNumberInput(e.target.value); - setScript((prev) => ({ - ...prev, - autoQueryInterval: - cleaned === "" ? undefined : parseInt(cleaned, 10), - })); - }} - onBlur={(e) => { - // 失焦时:严格验证并约束范围 - const validated = validateAndClampInterval( - e.target.value, - ); - setScript({ ...script, autoQueryInterval: validated }); - }} - /> -

- {t("usageScript.autoQueryIntervalHint")} -

-
-
+ {/* 凭证配置 */} + {shouldShowCredentialsConfig && ( +
+

+ {t("usageScript.credentialsConfig")} +

- {/* 脚本说明 */} -
-

- {t("usageScript.scriptHelp")} -

-
-
- {t("usageScript.configFormat")} -
-                      {`({
+                                
+ {selectedTemplate === TEMPLATE_KEYS.GENERAL && ( + <> +
+ +
+ + setScript({ ...script, apiKey: e.target.value }) + } + placeholder="sk-xxxxx" + autoComplete="off" + className="bg-card border-border-default" + /> + {script.apiKey && ( + + )} +
+
+ +
+ + + setScript({ ...script, baseUrl: e.target.value }) + } + placeholder="https://api.example.com" + autoComplete="off" + className="bg-card border-border-default" + /> +
+ + )} + + {selectedTemplate === TEMPLATE_KEYS.NEW_API && ( + <> +
+ + + setScript({ ...script, baseUrl: e.target.value }) + } + placeholder="https://api.newapi.com" + autoComplete="off" + className="bg-card border-border-default" + /> +
+ +
+ +
+ + setScript({ ...script, accessToken: e.target.value }) + } + placeholder={t("usageScript.accessTokenPlaceholder")} + autoComplete="off" + className="bg-card border-border-default" + /> + {script.accessToken && ( + + )} +
+
+ +
+ + + setScript({ ...script, userId: e.target.value }) + } + placeholder={t("usageScript.userIdPlaceholder")} + autoComplete="off" + className="bg-card border-border-default" + /> +
+ + )} +
+
+ )} +
+ + {/* 脚本配置 */} +
+
+

+ {t("usageScript.scriptConfig")} +

+

+ {t("usageScript.variablesHint")} +

+
+ +
+
+ + { + setScript({ + ...script, + request: { ...script.request, url: e.target.value }, + }); + }} + placeholder={t("usageScript.requestUrlPlaceholder")} + className="bg-card border-border-default" + /> +
+ +
+
+ + { + setScript({ + ...script, + request: { + ...script.request, + method: e.target.value.toUpperCase(), + }, + }); + }} + placeholder="GET / POST" + className="bg-card border-border-default" + /> +
+ +
+ + + setScript({ + ...script, + timeout: validateTimeout(e.target.value), + }) + } + onBlur={(e) => + setScript({ + ...script, + timeout: validateTimeout(e.target.value), + }) + } + className="bg-card border-border-default" + /> +
+
+ +
+ + { + try { + const parsed = JSON.parse(value || "{}"); + setScript({ + ...script, + request: { ...script.request, headers: parsed }, + }); + } catch (error) { + console.error("Invalid headers JSON", error); + } + }} + height={180} + /> +
+ +
+ + { + try { + const parsed = value?.trim() === "" ? undefined : JSON.parse(value); + setScript({ + ...script, + request: { ...script.request, body: parsed }, + }); + } catch (error) { + toast.error( + t("usageScript.invalidJson") || "Body 必须是合法 JSON", + ); + } + }} + height={220} + /> +
+ +
+ + + setScript({ + ...script, + autoIntervalMinutes: validateAndClampInterval(e.target.value), + }) + } + onBlur={(e) => + setScript({ + ...script, + autoIntervalMinutes: validateAndClampInterval(e.target.value), + }) + } + className="bg-card border-border-default" + /> +

+ {t("usageScript.autoQueryIntervalHint")} +

+
+
+
+ + {/* 提取器代码 */} +
+
+ +
+ {t("usageScript.extractorHint")} +
+
+ setScript({ ...script, code: value })} + height={480} + language="javascript" + showMinimap={false} + /> +
+ + {/* 帮助信息 */} +
+

{t("usageScript.scriptHelp")}

+
+
+ {t("usageScript.configFormat")} +
+                                    {`({
   request: {
     url: "{{baseUrl}}/api/usage",
     method: "POST",
     headers: {
       "Authorization": "Bearer {{apiKey}}",
       "User-Agent": "cc-switch/1.0"
-    },
-    body: JSON.stringify({ key: "value" })  // ${t("usageScript.commentOptional")}
+    }
   },
   extractor: function(response) {
-    // ${t("usageScript.commentResponseIsJson")}
     return {
       isValid: !response.error,
       remaining: response.balance,
@@ -654,80 +712,39 @@ const UsageScriptModal: React.FC = ({
     };
   }
 })`}
-                    
-
+ +
-
- {t("usageScript.extractorFormat")} -
    -
  • {t("usageScript.fieldIsValid")}
  • -
  • {t("usageScript.fieldInvalidMessage")}
  • -
  • {t("usageScript.fieldRemaining")}
  • -
  • {t("usageScript.fieldUnit")}
  • -
  • {t("usageScript.fieldPlanName")}
  • -
  • {t("usageScript.fieldTotal")}
  • -
  • {t("usageScript.fieldUsed")}
  • -
  • {t("usageScript.fieldExtra")}
  • -
-
+
+ {t("usageScript.extractorFormat")} +
    +
  • {t("usageScript.fieldIsValid")}
  • +
  • {t("usageScript.fieldInvalidMessage")}
  • +
  • {t("usageScript.fieldRemaining")}
  • +
  • {t("usageScript.fieldUnit")}
  • +
  • {t("usageScript.fieldPlanName")}
  • +
  • {t("usageScript.fieldTotal")}
  • +
  • {t("usageScript.fieldUsed")}
  • +
  • {t("usageScript.fieldExtra")}
  • +
+
-
- {t("usageScript.tips")} -
    -
  • - {t("usageScript.tip1", { - apiKey: "{{apiKey}}", - baseUrl: "{{baseUrl}}", - })} -
  • -
  • {t("usageScript.tip2")}
  • -
  • {t("usageScript.tip3")}
  • -
-
+
+ {t("usageScript.tips")} +
    +
  • + {t("usageScript.tip1", { apiKey: "{{apiKey}}", baseUrl: "{{baseUrl}}" })} +
  • +
  • {t("usageScript.tip2")}
  • +
  • {t("usageScript.tip3")}
  • +
+
+
+
-
- - )} -
- - {/* Footer */} - - {/* Left side - Test and Format buttons */} -
- - -
- - {/* Right side - Cancel and Save buttons */} -
- - -
-
- - - ); + )} + + ); }; export default UsageScriptModal; diff --git a/src/components/providers/ProviderActions.tsx b/src/components/providers/ProviderActions.tsx index 952a27e..1f9c1f0 100644 --- a/src/components/providers/ProviderActions.tsx +++ b/src/components/providers/ProviderActions.tsx @@ -1,4 +1,4 @@ -import { BarChart3, Check, Edit, Play, Trash2 } from "lucide-react"; +import { BarChart3, Check, Copy, Edit, Play, Trash2 } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; @@ -7,6 +7,7 @@ interface ProviderActionsProps { isCurrent: boolean; onSwitch: () => void; onEdit: () => void; + onDuplicate: () => void; onConfigureUsage: () => void; onDelete: () => void; } @@ -15,6 +16,7 @@ export function ProviderActions({ isCurrent, onSwitch, onEdit, + onDuplicate, onConfigureUsage, onDelete, }: ProviderActionsProps) { @@ -56,6 +58,15 @@ export function ProviderActions({ + + - - -
+ +
@@ -210,23 +185,28 @@ export function ProviderCard({
-
- +
+
+ +
- onSwitch(provider)} - onEdit={() => onEdit(provider)} - onConfigureUsage={() => onConfigureUsage(provider)} - onDelete={() => onDelete(provider)} - /> +
+ onSwitch(provider)} + onEdit={() => onEdit(provider)} + onDuplicate={() => onDuplicate(provider)} + onConfigureUsage={() => onConfigureUsage(provider)} + onDelete={() => onDelete(provider)} + /> +
diff --git a/src/components/providers/ProviderList.tsx b/src/components/providers/ProviderList.tsx index 580b358..79f5dd7 100644 --- a/src/components/providers/ProviderList.tsx +++ b/src/components/providers/ProviderList.tsx @@ -16,7 +16,6 @@ interface ProviderListProps { providers: Record; currentProviderId: string; appId: AppId; - isEditMode?: boolean; onSwitch: (provider: Provider) => void; onEdit: (provider: Provider) => void; onDelete: (provider: Provider) => void; @@ -31,7 +30,6 @@ export function ProviderList({ providers, currentProviderId, appId, - isEditMode = false, onSwitch, onEdit, onDelete, @@ -73,14 +71,13 @@ export function ProviderList({ items={sortedProviders.map((provider) => provider.id)} strategy={verticalListSortingStrategy} > -
+
{sortedProviders.map((provider) => ( void; onEdit: (provider: Provider) => void; onDelete: (provider: Provider) => void; @@ -112,7 +108,6 @@ function SortableProviderCard({ provider, isCurrent, appId, - isEditMode, onSwitch, onEdit, onDelete, @@ -140,7 +135,6 @@ function SortableProviderCard({ provider={provider} isCurrent={isCurrent} appId={appId} - isEditMode={isEditMode} onSwitch={onSwitch} onEdit={onEdit} onDelete={onDelete} diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 98a726f..79e28e4 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -14,13 +14,14 @@ const DialogClose = DialogPrimitive.Close; const DialogOverlay = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - zIndex?: "base" | "nested" | "alert"; + zIndex?: "base" | "nested" | "alert" | "top"; } >(({ className, zIndex = "base", ...props }, ref) => { const zIndexMap = { base: "z-40", nested: "z-50", alert: "z-[60]", + top: "z-[110]", }; return ( @@ -40,22 +41,32 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; const DialogContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - zIndex?: "base" | "nested" | "alert"; + zIndex?: "base" | "nested" | "alert" | "top"; + variant?: "default" | "fullscreen"; + overlayClassName?: string; } ->(({ className, children, zIndex = "base", ...props }, ref) => { +>(({ className, children, zIndex = "base", variant = "default", overlayClassName, ...props }, ref) => { const zIndexMap = { base: "z-40", nested: "z-50", alert: "z-[60]", + top: "z-[110]", }; + const variantClass = { + default: + "fixed left-1/2 top-1/2 flex flex-col w-full max-w-lg max-h-[90vh] translate-x-[-50%] translate-y-[-50%] border border-border-default bg-white dark:bg-gray-900 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", + fullscreen: + "fixed inset-0 flex flex-col w-screen h-screen translate-x-0 translate-y-0 bg-background text-foreground p-0 sm:rounded-none shadow-none", + }[variant]; + return ( - +