mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-27 08:14:33 +08:00
feat(ui): add merge preview to common config editor components
Add real-time merge preview functionality to all config editor components (Claude, Codex, Gemini) that shows the final merged configuration when common config is enabled. UI changes: - Add finalConfig/finalEnv prop to display merged result - Add toggle button to show/hide merge preview (Eye/EyeOff icons) - Auto-show preview when common config is enabled - Display custom config editor with label when preview is visible - Show read-only merged preview below custom config - Add visual indicators: "只读" badge, green hint text The preview helps users understand how their custom configuration combines with the shared common config template before saving.
This commit is contained in:
@@ -30,6 +30,9 @@ interface CodexConfigEditorProps {
|
||||
onExtract?: () => void;
|
||||
|
||||
isExtracting?: boolean;
|
||||
|
||||
/** 最终合并后的配置(只读预览) */
|
||||
finalConfig?: string;
|
||||
}
|
||||
|
||||
const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
@@ -47,6 +50,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
configError,
|
||||
onExtract,
|
||||
isExtracting,
|
||||
finalConfig,
|
||||
}) => {
|
||||
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
|
||||
|
||||
@@ -76,6 +80,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
onEditCommonConfig={() => setIsCommonConfigModalOpen(true)}
|
||||
commonConfigError={commonConfigError}
|
||||
configError={configError}
|
||||
finalConfig={finalConfig}
|
||||
/>
|
||||
|
||||
{/* Common Config Modal */}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import JsonEditor from "@/components/JsonEditor";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
interface CodexAuthSectionProps {
|
||||
value: string;
|
||||
@@ -83,6 +85,8 @@ interface CodexConfigSectionProps {
|
||||
onEditCommonConfig: () => void;
|
||||
commonConfigError?: string;
|
||||
configError?: string;
|
||||
/** 最终合并后的配置(只读预览) */
|
||||
finalConfig?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,9 +100,11 @@ export const CodexConfigSection: React.FC<CodexConfigSectionProps> = ({
|
||||
onEditCommonConfig,
|
||||
commonConfigError,
|
||||
configError,
|
||||
finalConfig,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
||||
@@ -115,6 +121,13 @@ export const CodexConfigSection: React.FC<CodexConfigSectionProps> = ({
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
// 当启用通用配置时,自动显示预览
|
||||
useEffect(() => {
|
||||
if (useCommonConfig && finalConfig) {
|
||||
setShowPreview(true);
|
||||
}
|
||||
}, [useCommonConfig, finalConfig]);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -136,7 +149,32 @@ export const CodexConfigSection: React.FC<CodexConfigSectionProps> = ({
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{useCommonConfig && finalConfig && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPreview(!showPreview)}
|
||||
className="inline-flex items-center gap-1 text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
|
||||
>
|
||||
{showPreview ? (
|
||||
<>
|
||||
<EyeOff className="w-3 h-3" />
|
||||
{t("codexConfig.hidePreview", {
|
||||
defaultValue: "隐藏合并预览",
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Eye className="w-3 h-3" />
|
||||
{t("codexConfig.showPreview", {
|
||||
defaultValue: "显示合并预览",
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onEditCommonConfig}
|
||||
@@ -152,15 +190,57 @@ export const CodexConfigSection: React.FC<CodexConfigSectionProps> = ({
|
||||
</p>
|
||||
)}
|
||||
|
||||
<JsonEditor
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder=""
|
||||
darkMode={isDarkMode}
|
||||
rows={8}
|
||||
showValidation={false}
|
||||
language="javascript"
|
||||
/>
|
||||
{/* 自定义配置编辑器 */}
|
||||
<div className="space-y-1">
|
||||
{useCommonConfig && showPreview && (
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
{t("codexConfig.customConfig", {
|
||||
defaultValue: "自定义配置(覆盖通用配置)",
|
||||
})}
|
||||
</Label>
|
||||
)}
|
||||
<JsonEditor
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder=""
|
||||
darkMode={isDarkMode}
|
||||
rows={useCommonConfig && showPreview ? 6 : 8}
|
||||
showValidation={false}
|
||||
language="javascript"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 合并预览(只读)- 放在自定义配置下面 */}
|
||||
{useCommonConfig && showPreview && finalConfig && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
{t("codexConfig.mergedPreview", {
|
||||
defaultValue: "合并预览(只读)",
|
||||
})}
|
||||
</Label>
|
||||
<span className="text-xs text-green-500 dark:text-green-400">
|
||||
{t("codexConfig.mergedPreviewHint", {
|
||||
defaultValue: "通用配置 + 自定义配置 = 最终配置",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<JsonEditor
|
||||
value={finalConfig}
|
||||
onChange={() => {}} // 只读
|
||||
darkMode={isDarkMode}
|
||||
rows={6}
|
||||
showValidation={false}
|
||||
language="javascript"
|
||||
readOnly={true}
|
||||
/>
|
||||
<div className="absolute top-2 right-2 px-2 py-0.5 bg-green-500/10 text-green-600 dark:text-green-400 text-xs rounded">
|
||||
{t("common.readonly", { defaultValue: "只读" })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{configError && (
|
||||
<p className="text-xs text-red-500 dark:text-red-400">{configError}</p>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
|
||||
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Save, Download, Loader2 } from "lucide-react";
|
||||
import { Save, Download, Loader2, Eye, EyeOff } from "lucide-react";
|
||||
import JsonEditor from "@/components/JsonEditor";
|
||||
|
||||
interface CommonConfigEditorProps {
|
||||
@@ -19,6 +19,8 @@ interface CommonConfigEditorProps {
|
||||
onModalClose: () => void;
|
||||
onExtract?: () => void;
|
||||
isExtracting?: boolean;
|
||||
/** 最终合并后的配置(只读预览) */
|
||||
finalConfig?: string;
|
||||
}
|
||||
|
||||
export function CommonConfigEditor({
|
||||
@@ -34,9 +36,11 @@ export function CommonConfigEditor({
|
||||
onModalClose,
|
||||
onExtract,
|
||||
isExtracting,
|
||||
finalConfig,
|
||||
}: CommonConfigEditorProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
||||
@@ -53,6 +57,13 @@ export function CommonConfigEditor({
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
// 当启用通用配置时,自动显示预览
|
||||
useEffect(() => {
|
||||
if (useCommonConfig && finalConfig) {
|
||||
setShowPreview(true);
|
||||
}
|
||||
}, [useCommonConfig, finalConfig]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
@@ -75,7 +86,32 @@ export function CommonConfigEditor({
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{useCommonConfig && finalConfig && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPreview(!showPreview)}
|
||||
className="inline-flex items-center gap-1 text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
|
||||
>
|
||||
{showPreview ? (
|
||||
<>
|
||||
<EyeOff className="w-3 h-3" />
|
||||
{t("claudeConfig.hidePreview", {
|
||||
defaultValue: "隐藏合并预览",
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Eye className="w-3 h-3" />
|
||||
{t("claudeConfig.showPreview", {
|
||||
defaultValue: "显示合并预览",
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onEditClick}
|
||||
@@ -91,20 +127,63 @@ export function CommonConfigEditor({
|
||||
{commonConfigError}
|
||||
</p>
|
||||
)}
|
||||
<JsonEditor
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={`{
|
||||
|
||||
{/* 自定义配置编辑器 */}
|
||||
<div className="space-y-1">
|
||||
{useCommonConfig && showPreview && (
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
{t("claudeConfig.customConfig", {
|
||||
defaultValue: "自定义配置(覆盖通用配置)",
|
||||
})}
|
||||
</Label>
|
||||
)}
|
||||
<JsonEditor
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={`{
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://your-api-endpoint.com",
|
||||
"ANTHROPIC_AUTH_TOKEN": "your-api-key-here"
|
||||
}
|
||||
}`}
|
||||
darkMode={isDarkMode}
|
||||
rows={14}
|
||||
showValidation={true}
|
||||
language="json"
|
||||
/>
|
||||
darkMode={isDarkMode}
|
||||
rows={useCommonConfig && showPreview ? 10 : 14}
|
||||
showValidation={true}
|
||||
language="json"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 合并预览(只读)- 放在自定义配置下面 */}
|
||||
{useCommonConfig && showPreview && finalConfig && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
{t("claudeConfig.mergedPreview", {
|
||||
defaultValue: "合并预览(只读)",
|
||||
})}
|
||||
</Label>
|
||||
<span className="text-xs text-green-500 dark:text-green-400">
|
||||
{t("claudeConfig.mergedPreviewHint", {
|
||||
defaultValue: "通用配置 + 自定义配置 = 最终配置",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<JsonEditor
|
||||
value={finalConfig}
|
||||
onChange={() => {}} // 只读
|
||||
darkMode={isDarkMode}
|
||||
rows={8}
|
||||
showValidation={false}
|
||||
language="json"
|
||||
readOnly={true}
|
||||
/>
|
||||
<div className="absolute top-2 right-2 px-2 py-0.5 bg-green-500/10 text-green-600 dark:text-green-400 text-xs rounded">
|
||||
{t("common.readonly", { defaultValue: "只读" })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FullScreenPanel
|
||||
|
||||
@@ -17,6 +17,8 @@ interface GeminiConfigEditorProps {
|
||||
configError: string;
|
||||
onExtract?: () => void;
|
||||
isExtracting?: boolean;
|
||||
/** 最终合并后的 env 配置(只读预览) */
|
||||
finalEnv?: string;
|
||||
}
|
||||
|
||||
const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
|
||||
@@ -34,6 +36,7 @@ const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
|
||||
configError,
|
||||
onExtract,
|
||||
isExtracting,
|
||||
finalEnv,
|
||||
}) => {
|
||||
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
|
||||
|
||||
@@ -56,6 +59,7 @@ const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
|
||||
onCommonConfigToggle={onCommonConfigToggle}
|
||||
onEditCommonConfig={() => setIsCommonConfigModalOpen(true)}
|
||||
commonConfigError={commonConfigError}
|
||||
finalEnv={finalEnv}
|
||||
/>
|
||||
|
||||
{/* Config JSON Section */}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import JsonEditor from "@/components/JsonEditor";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
interface GeminiEnvSectionProps {
|
||||
value: string;
|
||||
@@ -11,6 +13,8 @@ interface GeminiEnvSectionProps {
|
||||
onCommonConfigToggle: (checked: boolean) => void;
|
||||
onEditCommonConfig: () => void;
|
||||
commonConfigError?: string;
|
||||
/** 最终合并后的 env 配置(只读预览) */
|
||||
finalEnv?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,9 +29,11 @@ export const GeminiEnvSection: React.FC<GeminiEnvSectionProps> = ({
|
||||
onCommonConfigToggle,
|
||||
onEditCommonConfig,
|
||||
commonConfigError,
|
||||
finalEnv,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
||||
@@ -44,6 +50,13 @@ export const GeminiEnvSection: React.FC<GeminiEnvSectionProps> = ({
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
// 当启用通用配置时,自动显示预览
|
||||
useEffect(() => {
|
||||
if (useCommonConfig && finalEnv) {
|
||||
setShowPreview(true);
|
||||
}
|
||||
}, [useCommonConfig, finalEnv]);
|
||||
|
||||
const handleChange = (newValue: string) => {
|
||||
onChange(newValue);
|
||||
if (onBlur) {
|
||||
@@ -74,7 +87,32 @@ export const GeminiEnvSection: React.FC<GeminiEnvSectionProps> = ({
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{useCommonConfig && finalEnv && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPreview(!showPreview)}
|
||||
className="inline-flex items-center gap-1 text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
|
||||
>
|
||||
{showPreview ? (
|
||||
<>
|
||||
<EyeOff className="w-3 h-3" />
|
||||
{t("geminiConfig.hidePreview", {
|
||||
defaultValue: "隐藏合并预览",
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Eye className="w-3 h-3" />
|
||||
{t("geminiConfig.showPreview", {
|
||||
defaultValue: "显示合并预览",
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onEditCommonConfig}
|
||||
@@ -92,17 +130,59 @@ export const GeminiEnvSection: React.FC<GeminiEnvSectionProps> = ({
|
||||
</p>
|
||||
)}
|
||||
|
||||
<JsonEditor
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
placeholder={`GOOGLE_GEMINI_BASE_URL=https://your-api-endpoint.com/
|
||||
{/* 自定义配置编辑器 */}
|
||||
<div className="space-y-1">
|
||||
{useCommonConfig && showPreview && (
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
{t("geminiConfig.customConfig", {
|
||||
defaultValue: "自定义配置(覆盖通用配置)",
|
||||
})}
|
||||
</Label>
|
||||
)}
|
||||
<JsonEditor
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
placeholder={`GOOGLE_GEMINI_BASE_URL=https://your-api-endpoint.com/
|
||||
GEMINI_API_KEY=sk-your-api-key-here
|
||||
GEMINI_MODEL=gemini-3-pro-preview`}
|
||||
darkMode={isDarkMode}
|
||||
rows={6}
|
||||
showValidation={false}
|
||||
language="javascript"
|
||||
/>
|
||||
darkMode={isDarkMode}
|
||||
rows={useCommonConfig && showPreview ? 4 : 6}
|
||||
showValidation={false}
|
||||
language="javascript"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 合并预览(只读)- 放在自定义配置下面 */}
|
||||
{useCommonConfig && showPreview && finalEnv && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
{t("geminiConfig.mergedPreview", {
|
||||
defaultValue: "合并预览(只读)",
|
||||
})}
|
||||
</Label>
|
||||
<span className="text-xs text-green-500 dark:text-green-400">
|
||||
{t("geminiConfig.mergedPreviewHint", {
|
||||
defaultValue: "通用配置 + 自定义配置 = 最终配置",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<JsonEditor
|
||||
value={finalEnv}
|
||||
onChange={() => {}} // 只读
|
||||
darkMode={isDarkMode}
|
||||
rows={4}
|
||||
showValidation={false}
|
||||
language="javascript"
|
||||
readOnly={true}
|
||||
/>
|
||||
<div className="absolute top-2 right-2 px-2 py-0.5 bg-green-500/10 text-green-600 dark:text-green-400 text-xs rounded">
|
||||
{t("common.readonly", { defaultValue: "只读" })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<p className="text-xs text-red-500 dark:text-red-400">{error}</p>
|
||||
|
||||
Reference in New Issue
Block a user