feat: 系统托盘 (#12)
* feat: 系统托盘 1. 添加系统托盘 2. 托盘添加切换供应商功能 3. 整理组件目录 * feat: 优化系统托盘菜单结构 - 扁平化Claude和Codex的菜单结构,直接将所有供应商添加到主菜单,简化用户交互。 - 添加无供应商时的提示信息,提升用户体验。 - 更新分隔符文本以增强可读性。 * feat: integrate Tailwind CSS and Lucide icons - Added Tailwind CSS for styling and layout improvements. - Integrated Lucide icons for enhanced UI elements. - Updated project structure by removing unused CSS files and components. - Refactored configuration files to support new styling and component structure. - Introduced new components for managing providers with improved UI interactions. * fix: 修复类型声明和分隔符实现问题 - 修复 updateTrayMenu 返回类型不一致(Promise<void> -> Promise<boolean>) - 添加缺失的 UnlistenFn 类型导入 - 使用 MenuBuilder.separator() 替代文本分隔符 --------- Co-authored-by: farion1231 <farion1231@gmail.c
This commit is contained in:
@@ -1,268 +0,0 @@
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 0;
|
||||
width: 90%;
|
||||
max-width: 640px;
|
||||
max-height: 90vh;
|
||||
overflow: hidden; /* 由 body 滚动,标题栏固定 */
|
||||
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2);
|
||||
position: relative;
|
||||
z-index: 1001;
|
||||
display: flex; /* 纵向布局,便于底栏固定 */
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 模拟窗口标题栏 */
|
||||
.modal-titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 3rem; /* 与主窗口标题栏一致 */
|
||||
padding: 0 12px; /* 接近主头部的水平留白 */
|
||||
background: #3498db; /* 与 .app-header 相同 */
|
||||
color: #fff;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
/* 左侧占位以保证标题居中(与右侧关闭按钮宽度相当) */
|
||||
.modal-spacer {
|
||||
width: 32px;
|
||||
flex: 0 0 32px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-close-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.modal-form {
|
||||
/* 表单外层包裹 body + footer */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0; /* 允许子元素正确计算高度 */
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.25rem 1.5rem 1.5rem;
|
||||
overflow: auto; /* 仅内容区滚动 */
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #fee;
|
||||
color: #c33;
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid #fcc;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.presets {
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 1px solid #ecf0f1;
|
||||
}
|
||||
|
||||
.presets label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #555;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.preset-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.preset-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid #3498db;
|
||||
background: white;
|
||||
color: #3498db;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.preset-btn:hover,
|
||||
.preset-btn.selected {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 官方按钮橙色主题(Anthropic 风格) */
|
||||
.preset-btn.official {
|
||||
border: 1px solid #d97706;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.preset-btn.official:hover,
|
||||
.preset-btn.official.selected {
|
||||
background: #d97706;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* API Key 输入框容器 - 预留空间避免抖动 */
|
||||
.form-group.api-key-group {
|
||||
min-height: 88px; /* 固定高度:label + input + 间距 */
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.form-group.api-key-group.hidden {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.625rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.95rem;
|
||||
transition: border-color 0.2s;
|
||||
background: white;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
/* 固定在弹窗底部(非滚动区) */
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-top: 1px solid #ecf0f1;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.cancel-btn,
|
||||
.submit-btn {
|
||||
padding: 0.625rem 1.25rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: #ecf0f1;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.cancel-btn:hover {
|
||||
background: #bdc3c7;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background: #27ae60;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
background: #229954;
|
||||
}
|
||||
|
||||
.field-hint {
|
||||
display: block;
|
||||
margin-top: 0.25rem;
|
||||
color: #7f8c8d;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* 添加标签和选择框的样式 */
|
||||
.label-with-checkbox {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.label-with-checkbox label:first-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
font-weight: normal;
|
||||
margin-bottom: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
width: auto;
|
||||
margin: 2px;
|
||||
cursor: pointer;
|
||||
transform: translateY(2px);
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/* 药丸式切换按钮 */
|
||||
.switcher-pills {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
padding: 6px 8px;
|
||||
border-radius: 50px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.switcher-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 200ms ease;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.switcher-pill:hover:not(.active) {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.switcher-pill.active {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: white;
|
||||
box-shadow:
|
||||
inset 0 1px 3px rgba(0, 0, 0, 0.1),
|
||||
0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.pill-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
opacity: 0.4;
|
||||
transition: all 200ms ease;
|
||||
}
|
||||
|
||||
.switcher-pill.active .pill-dot {
|
||||
opacity: 1;
|
||||
box-shadow: 0 0 8px currentColor;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.pills-divider {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AppType } from "../lib/tauri-api";
|
||||
import "./AppSwitcher.css";
|
||||
import { Terminal, Code2 } from "lucide-react";
|
||||
|
||||
interface AppSwitcherProps {
|
||||
activeApp: AppType;
|
||||
@@ -13,22 +13,30 @@ export function AppSwitcher({ activeApp, onSwitch }: AppSwitcherProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="switcher-pills">
|
||||
<div className="inline-flex bg-[var(--color-bg-tertiary)] rounded-lg p-1 gap-1">
|
||||
<button
|
||||
type="button"
|
||||
className={`switcher-pill ${activeApp === "claude" ? "active" : ""}`}
|
||||
onClick={() => handleSwitch("claude")}
|
||||
className={`inline-flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
|
||||
activeApp === "claude"
|
||||
? "bg-white text-[var(--color-text-primary)] shadow-sm"
|
||||
: "text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-white/50"
|
||||
}`}
|
||||
>
|
||||
<span className="pill-dot" />
|
||||
<Code2 size={16} />
|
||||
<span>Claude Code</span>
|
||||
</button>
|
||||
<div className="pills-divider" />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`switcher-pill ${activeApp === "codex" ? "active" : ""}`}
|
||||
onClick={() => handleSwitch("codex")}
|
||||
className={`inline-flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
|
||||
activeApp === "codex"
|
||||
? "bg-white text-[var(--color-text-primary)] shadow-sm"
|
||||
: "text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-white/50"
|
||||
}`}
|
||||
>
|
||||
<span className="pill-dot" />
|
||||
<Terminal size={16} />
|
||||
<span>Codex</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
.confirm-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.confirm-dialog {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
animation: confirmSlideIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes confirmSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-header {
|
||||
padding: 1.5rem 1.5rem 1rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.confirm-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.confirm-content {
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.confirm-content p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.confirm-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem 1.5rem 1.5rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
transition:
|
||||
background-color 0.2s,
|
||||
transform 0.1s;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.confirm-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.confirm-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: #f8f9fa;
|
||||
color: #6c757d;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.cancel-btn:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.confirm-btn-primary {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.confirm-btn-primary:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.confirm-btn:focus {
|
||||
outline: 2px solid #007bff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import "./ConfirmDialog.css";
|
||||
import { AlertTriangle, X } from "lucide-react";
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -23,25 +23,52 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="confirm-overlay">
|
||||
<div className="confirm-dialog">
|
||||
<div className="confirm-header">
|
||||
<h3>{title}</h3>
|
||||
</div>
|
||||
<div className="confirm-content">
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
<div className="confirm-actions">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||
onClick={onCancel}
|
||||
/>
|
||||
|
||||
{/* Dialog */}
|
||||
<div className="relative bg-white rounded-xl shadow-lg max-w-md w-full mx-4 overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-[var(--color-border)]">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-[var(--color-error-light)] rounded-full flex items-center justify-center">
|
||||
<AlertTriangle size={20} className="text-[var(--color-error)]" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-[var(--color-text-primary)]">
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
className="confirm-btn cancel-btn"
|
||||
onClick={onCancel}
|
||||
className="p-1 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-tertiary)] rounded-md transition-colors"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
<p className="text-[var(--color-text-secondary)] leading-relaxed">
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-end gap-3 p-6 border-t border-[var(--color-border)] bg-[var(--color-bg-tertiary)]">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-sm font-medium text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-white rounded-md transition-colors"
|
||||
autoFocus
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
<button
|
||||
className="confirm-btn confirm-btn-primary"
|
||||
onClick={onConfirm}
|
||||
className="px-4 py-2 text-sm font-medium bg-[var(--color-error)] text-white hover:bg-[var(--color-error)]/90 rounded-md transition-colors"
|
||||
>
|
||||
{confirmText}
|
||||
</button>
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
} from "../utils/providerConfigUtils";
|
||||
import { providerPresets } from "../config/providerPresets";
|
||||
import { codexProviderPresets } from "../config/codexProviderPresets";
|
||||
import "./AddProviderModal.css";
|
||||
import JsonEditor from "./JsonEditor";
|
||||
import { X, AlertCircle, Save, Zap } from "lucide-react";
|
||||
|
||||
interface ProviderFormProps {
|
||||
appType?: AppType;
|
||||
@@ -49,7 +49,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
const [codexApiKey, setCodexApiKey] = useState("");
|
||||
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
||||
const [selectedCodexPreset, setSelectedCodexPreset] = useState<number | null>(
|
||||
showPresets && isCodex ? -1 : null,
|
||||
showPresets && isCodex ? -1 : null
|
||||
);
|
||||
|
||||
// 初始化 Codex 配置
|
||||
@@ -74,7 +74,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
const [disableCoAuthored, setDisableCoAuthored] = useState(false);
|
||||
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
||||
const [selectedPreset, setSelectedPreset] = useState<number | null>(
|
||||
showPresets ? -1 : null,
|
||||
showPresets ? -1 : null
|
||||
);
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
|
||||
@@ -155,7 +155,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
};
|
||||
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
@@ -188,7 +188,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
// 更新JSON配置
|
||||
const updatedConfig = updateCoAuthoredSetting(
|
||||
formData.settingsConfig,
|
||||
checked,
|
||||
checked
|
||||
);
|
||||
setFormData({
|
||||
...formData,
|
||||
@@ -231,7 +231,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
// Codex: 应用预设
|
||||
const applyCodexPreset = (
|
||||
preset: (typeof codexProviderPresets)[0],
|
||||
index: number,
|
||||
index: number
|
||||
) => {
|
||||
const authString = JSON.stringify(preset.auth || {}, null, 2);
|
||||
setCodexAuth(authString);
|
||||
@@ -269,7 +269,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
const configString = setApiKeyInConfig(
|
||||
formData.settingsConfig,
|
||||
key.trim(),
|
||||
{ createIfMissing: selectedPreset !== null && selectedPreset !== -1 },
|
||||
{ createIfMissing: selectedPreset !== null && selectedPreset !== -1 }
|
||||
);
|
||||
|
||||
// 更新表单配置
|
||||
@@ -329,7 +329,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
const parsedKey = getApiKeyFromConfig(
|
||||
JSON.stringify(initialData.settingsConfig),
|
||||
JSON.stringify(initialData.settingsConfig)
|
||||
);
|
||||
if (parsedKey) setApiKey(parsedKey);
|
||||
}
|
||||
@@ -350,130 +350,156 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="modal-overlay"
|
||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||
onMouseDown={(e) => {
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
}}
|
||||
>
|
||||
<div className="modal-content">
|
||||
<div className="modal-titlebar">
|
||||
<div className="modal-spacer" />
|
||||
<div className="modal-title" title={title}>
|
||||
{/* Backdrop */}
|
||||
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" />
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative bg-white rounded-xl shadow-lg max-w-3xl w-full mx-4 max-h-[90vh] overflow-hidden flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-[var(--color-border)]">
|
||||
<h2 className="text-xl font-semibold text-[var(--color-text-primary)]">
|
||||
{title}
|
||||
</div>
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
className="modal-close-btn"
|
||||
aria-label="关闭"
|
||||
onClick={onClose}
|
||||
title="关闭"
|
||||
className="p-1 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-tertiary)] rounded-md transition-colors"
|
||||
aria-label="关闭"
|
||||
>
|
||||
×
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="modal-form">
|
||||
<div className="modal-body">
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
<form onSubmit={handleSubmit} className="flex flex-col flex-1 min-h-0">
|
||||
<div className="flex-1 overflow-auto p-6 space-y-6">
|
||||
{error && (
|
||||
<div className="flex items-center gap-3 p-4 bg-[var(--color-error-light)] border border-[var(--color-error)]/20 rounded-lg">
|
||||
<AlertCircle
|
||||
size={20}
|
||||
className="text-[var(--color-error)] flex-shrink-0"
|
||||
/>
|
||||
<p className="text-[var(--color-error)] text-sm font-medium">
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showPresets && !isCodex && (
|
||||
<div className="presets">
|
||||
<label>选择配置类型</label>
|
||||
<div className="preset-buttons">
|
||||
<button
|
||||
type="button"
|
||||
className={`preset-btn ${
|
||||
selectedPreset === -1 ? "selected" : ""
|
||||
}`}
|
||||
onClick={handleCustomClick}
|
||||
>
|
||||
自定义
|
||||
</button>
|
||||
{providerPresets.map((preset, index) => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--color-text-primary)] mb-3">
|
||||
选择配置类型
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
selectedPreset === -1
|
||||
? "bg-[var(--color-primary)] text-white"
|
||||
: "bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] hover:bg-[var(--color-border)]"
|
||||
}`}
|
||||
onClick={handleCustomClick}
|
||||
>
|
||||
自定义
|
||||
</button>
|
||||
{providerPresets.map((preset, index) => (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
className={`preset-btn ${
|
||||
selectedPreset === index ? "selected" : ""
|
||||
} ${preset.isOfficial ? "official" : ""}`}
|
||||
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
selectedPreset === index
|
||||
? preset.isOfficial
|
||||
? "bg-[var(--color-warning)] text-white"
|
||||
: "bg-[var(--color-primary)] text-white"
|
||||
: "bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] hover:bg-[var(--color-border)]"
|
||||
}`}
|
||||
onClick={() => applyPreset(preset, index)}
|
||||
>
|
||||
{preset.isOfficial && <Zap size={14} />}
|
||||
{preset.name}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{selectedPreset === -1 && (
|
||||
<small
|
||||
className="field-hint"
|
||||
style={{ marginTop: "8px", display: "block" }}
|
||||
>
|
||||
<p className="text-sm text-[var(--color-text-secondary)]">
|
||||
手动配置供应商,需要填写完整的配置信息
|
||||
</small>
|
||||
</p>
|
||||
)}
|
||||
{selectedPreset !== -1 && selectedPreset !== null && (
|
||||
<small
|
||||
className="field-hint"
|
||||
style={{ marginTop: "8px", display: "block" }}
|
||||
>
|
||||
<p className="text-sm text-[var(--color-text-secondary)]">
|
||||
{isOfficialPreset
|
||||
? "Claude 官方登录,不需要填写 API Key"
|
||||
: "使用预设配置,只需填写 API Key"}
|
||||
</small>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showPresets && isCodex && (
|
||||
<div className="presets">
|
||||
<label>选择配置类型</label>
|
||||
<div className="preset-buttons">
|
||||
<button
|
||||
type="button"
|
||||
className={`preset-btn ${
|
||||
selectedCodexPreset === -1 ? "selected" : ""
|
||||
}`}
|
||||
onClick={handleCodexCustomClick}
|
||||
>
|
||||
自定义
|
||||
</button>
|
||||
{codexProviderPresets.map((preset, index) => (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--color-text-primary)] mb-3">
|
||||
选择配置类型
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
className={`preset-btn ${
|
||||
selectedCodexPreset === index ? "selected" : ""
|
||||
} ${preset.isOfficial ? "official" : ""}`}
|
||||
onClick={() => applyCodexPreset(preset, index)}
|
||||
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
selectedCodexPreset === -1
|
||||
? "bg-[var(--color-primary)] text-white"
|
||||
: "bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] hover:bg-[var(--color-border)]"
|
||||
}`}
|
||||
onClick={handleCodexCustomClick}
|
||||
>
|
||||
{preset.name}
|
||||
自定义
|
||||
</button>
|
||||
))}
|
||||
{codexProviderPresets.map((preset, index) => (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
selectedCodexPreset === index
|
||||
? preset.isOfficial
|
||||
? "bg-[var(--color-warning)] text-white"
|
||||
: "bg-[var(--color-primary)] text-white"
|
||||
: "bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] hover:bg-[var(--color-border)]"
|
||||
}`}
|
||||
onClick={() => applyCodexPreset(preset, index)}
|
||||
>
|
||||
{preset.isOfficial && <Zap size={14} />}
|
||||
{preset.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{selectedCodexPreset === -1 && (
|
||||
<small
|
||||
className="field-hint"
|
||||
style={{ marginTop: "8px", display: "block" }}
|
||||
>
|
||||
<p className="text-sm text-[var(--color-text-secondary)]">
|
||||
手动配置供应商,需要填写完整的配置信息
|
||||
</small>
|
||||
</p>
|
||||
)}
|
||||
{selectedCodexPreset !== -1 && selectedCodexPreset !== null && (
|
||||
<small
|
||||
className="field-hint"
|
||||
style={{ marginTop: "8px", display: "block" }}
|
||||
>
|
||||
<p className="text-sm text-[var(--color-text-secondary)]">
|
||||
{isCodexOfficialPreset
|
||||
? "Codex 官方登录,不需要填写 API Key"
|
||||
: "使用预设配置,只需填写 API Key"}
|
||||
</small>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="name">供应商名称 *</label>
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||
>
|
||||
供应商名称 *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
@@ -483,14 +509,18 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
placeholder="例如:Anthropic 官方"
|
||||
required
|
||||
autoComplete="off"
|
||||
className="w-full px-3 py-2 border border-[var(--color-border)] rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)] transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isCodex && (
|
||||
<div
|
||||
className={`form-group api-key-group ${!showApiKey ? "hidden" : ""}`}
|
||||
>
|
||||
<label htmlFor="apiKey">API Key *</label>
|
||||
{!isCodex && showApiKey && (
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="apiKey"
|
||||
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||
>
|
||||
API Key *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="apiKey"
|
||||
@@ -503,24 +533,23 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
}
|
||||
disabled={isOfficialPreset}
|
||||
autoComplete="off"
|
||||
style={
|
||||
className={`w-full px-3 py-2 border rounded-lg text-sm transition-colors ${
|
||||
isOfficialPreset
|
||||
? {
|
||||
backgroundColor: "#f5f5f5",
|
||||
cursor: "not-allowed",
|
||||
color: "#999",
|
||||
}
|
||||
: {}
|
||||
}
|
||||
? "bg-[var(--color-bg-tertiary)] border-[var(--color-border)] text-[var(--color-text-tertiary)] cursor-not-allowed"
|
||||
: "border-[var(--color-border)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)]"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isCodex && (
|
||||
<div
|
||||
className={`form-group api-key-group ${!showCodexApiKey ? "hidden" : ""}`}
|
||||
>
|
||||
<label htmlFor="codexApiKey">API Key *</label>
|
||||
{isCodex && showCodexApiKey && (
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="codexApiKey"
|
||||
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||
>
|
||||
API Key *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="codexApiKey"
|
||||
@@ -538,21 +567,22 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
!isCodexOfficialPreset
|
||||
}
|
||||
autoComplete="off"
|
||||
style={
|
||||
className={`w-full px-3 py-2 border rounded-lg text-sm transition-colors ${
|
||||
isCodexOfficialPreset
|
||||
? {
|
||||
backgroundColor: "#f5f5f5",
|
||||
cursor: "not-allowed",
|
||||
color: "#999",
|
||||
}
|
||||
: {}
|
||||
}
|
||||
? "bg-[var(--color-bg-tertiary)] border-[var(--color-border)] text-[var(--color-text-tertiary)] cursor-not-allowed"
|
||||
: "border-[var(--color-border)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)]"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="websiteUrl">官网地址</label>
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="websiteUrl"
|
||||
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||
>
|
||||
官网地址
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
id="websiteUrl"
|
||||
@@ -561,15 +591,21 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
onChange={handleChange}
|
||||
placeholder="https://example.com(可选)"
|
||||
autoComplete="off"
|
||||
className="w-full px-3 py-2 border border-[var(--color-border)] rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)] transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Claude 或 Codex 的配置部分 */}
|
||||
{isCodex ? (
|
||||
// Codex: 双编辑器
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label htmlFor="codexAuth">auth.json (JSON) *</label>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="codexAuth"
|
||||
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||
>
|
||||
auth.json (JSON) *
|
||||
</label>
|
||||
<textarea
|
||||
id="codexAuth"
|
||||
value={codexAuth}
|
||||
@@ -591,47 +627,61 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
"OPENAI_API_KEY": "sk-your-api-key-here"
|
||||
}`}
|
||||
rows={6}
|
||||
style={{ fontFamily: "monospace", fontSize: "14px" }}
|
||||
required
|
||||
className="w-full px-3 py-2 border border-[var(--color-border)] rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)] transition-colors resize-y min-h-[8rem]"
|
||||
/>
|
||||
<small className="field-hint">Codex auth.json 配置内容</small>
|
||||
<p className="text-xs text-[var(--color-text-secondary)]">
|
||||
Codex auth.json 配置内容
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="codexConfig">config.toml (TOML)</label>
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="codexConfig"
|
||||
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||
>
|
||||
config.toml (TOML)
|
||||
</label>
|
||||
<textarea
|
||||
id="codexConfig"
|
||||
value={codexConfig}
|
||||
onChange={(e) => setCodexConfig(e.target.value)}
|
||||
placeholder={``}
|
||||
placeholder=""
|
||||
rows={8}
|
||||
className="w-full px-3 py-2 border border-[var(--color-border)] rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)] transition-colors resize-y min-h-[10rem]"
|
||||
/>
|
||||
<small className="field-hint">
|
||||
<p className="text-xs text-[var(--color-text-secondary)]">
|
||||
Codex config.toml 配置内容
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
) : (
|
||||
// Claude: 原有的单编辑器
|
||||
<div className="form-group">
|
||||
<div className="label-with-checkbox">
|
||||
<label htmlFor="settingsConfig">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label
|
||||
htmlFor="settingsConfig"
|
||||
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||
>
|
||||
Claude Code 配置 (JSON) *
|
||||
</label>
|
||||
<label className="checkbox-label">
|
||||
<label className="inline-flex items-center gap-2 text-sm text-[var(--color-text-secondary)] cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={disableCoAuthored}
|
||||
onChange={(e) => handleCoAuthoredToggle(e.target.checked)}
|
||||
className="w-4 h-4 text-[var(--color-primary)] bg-white border-[var(--color-border)] rounded focus:ring-[var(--color-primary)] focus:ring-2"
|
||||
/>
|
||||
禁止 Claude Code 签名
|
||||
</label>
|
||||
</div>
|
||||
<JsonEditor
|
||||
value={formData.settingsConfig}
|
||||
onChange={(value) => handleChange({
|
||||
target: { name: "settingsConfig", value }
|
||||
} as React.ChangeEvent<HTMLTextAreaElement>)}
|
||||
onChange={(value) =>
|
||||
handleChange({
|
||||
target: { name: "settingsConfig", value },
|
||||
} as React.ChangeEvent<HTMLTextAreaElement>)
|
||||
}
|
||||
placeholder={`{
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
|
||||
@@ -640,18 +690,27 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
}`}
|
||||
rows={12}
|
||||
/>
|
||||
<small className="field-hint">
|
||||
<p className="text-xs text-[var(--color-text-secondary)]">
|
||||
完整的 Claude Code settings.json 配置内容
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="cancel-btn" onClick={onClose}>
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end gap-3 p-6 border-t border-[var(--color-border)] bg-[var(--color-bg-tertiary)]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm font-medium text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-white rounded-lg transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button type="submit" className="submit-btn">
|
||||
<button
|
||||
type="submit"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-hover)] transition-colors text-sm font-medium"
|
||||
>
|
||||
<Save size={16} />
|
||||
{submitText}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
.provider-list {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.empty-state p:first-child {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.provider-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.provider-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border: 2px solid #ecf0f1;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.provider-item:hover {
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.provider-item.current {
|
||||
border-color: #27ae60;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.provider-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.provider-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.provider-name input[type="radio"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.provider-name input[type="radio"]:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.current-badge {
|
||||
background: #27ae60;
|
||||
color: white;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.provider-url {
|
||||
color: #7f8c8d;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.url-link {
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.url-link:hover {
|
||||
color: #2980b9;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.api-url {
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.provider-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
color: #555;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.response-time {
|
||||
color: #3498db;
|
||||
font-size: 0.85rem;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.provider-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.check-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid #f39c12;
|
||||
background: white;
|
||||
color: #f39c12;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.check-btn:hover:not(:disabled) {
|
||||
background: #f39c12;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.check-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.enable-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid #27ae60;
|
||||
background: white;
|
||||
color: #27ae60;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.enable-btn:hover:not(:disabled) {
|
||||
background: #27ae60;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.enable-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid #3498db;
|
||||
background: white;
|
||||
color: #3498db;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.edit-btn:hover:not(:disabled) {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.edit-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid #e74c3c;
|
||||
background: white;
|
||||
color: #e74c3c;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.delete-btn:hover:not(:disabled) {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.delete-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -1,6 +1,13 @@
|
||||
import React from "react";
|
||||
import { Provider } from "../types";
|
||||
import "./ProviderList.css";
|
||||
import {
|
||||
Play,
|
||||
Edit3,
|
||||
Trash2,
|
||||
ExternalLink,
|
||||
CheckCircle2,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
|
||||
interface ProviderListProps {
|
||||
providers: Record<string, Provider>;
|
||||
@@ -39,71 +46,104 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="provider-list">
|
||||
<div className="space-y-4">
|
||||
{Object.values(providers).length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<p>还没有添加任何供应商</p>
|
||||
<p>点击右上角的"添加供应商"按钮开始</p>
|
||||
<div className="text-center py-12">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-[var(--color-bg-tertiary)] rounded-full flex items-center justify-center">
|
||||
<Users size={24} className="text-[var(--color-text-tertiary)]" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-[var(--color-text-primary)] mb-2">
|
||||
还没有添加任何供应商
|
||||
</h3>
|
||||
<p className="text-[var(--color-text-secondary)] text-sm">
|
||||
点击右上角的"添加供应商"按钮开始配置您的第一个API供应商
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="provider-items">
|
||||
<div className="space-y-3">
|
||||
{Object.values(providers).map((provider) => {
|
||||
const isCurrent = provider.id === currentProviderId;
|
||||
const apiUrl = getApiUrl(provider);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={provider.id}
|
||||
className={`provider-item ${isCurrent ? "current" : ""}`}
|
||||
className={`bg-white rounded-lg border p-4 transition-all duration-200 ${
|
||||
isCurrent
|
||||
? "border-[var(--color-primary)] ring-1 ring-[var(--color-primary)]/20 bg-[var(--color-primary)]/5"
|
||||
: "border-[var(--color-border)] hover:border-[var(--color-border-hover)] hover:shadow-sm"
|
||||
}`}
|
||||
>
|
||||
<div className="provider-info">
|
||||
<div className="provider-name">
|
||||
<span>{provider.name}</span>
|
||||
{isCurrent && (
|
||||
<span className="current-badge">当前使用</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="provider-url">
|
||||
{provider.websiteUrl ? (
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleUrlClick(provider.websiteUrl!);
|
||||
}}
|
||||
className="url-link"
|
||||
title={`访问 ${provider.websiteUrl}`}
|
||||
>
|
||||
{provider.websiteUrl}
|
||||
</a>
|
||||
) : (
|
||||
<span className="api-url" title={getApiUrl(provider)}>
|
||||
{getApiUrl(provider)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="font-medium text-[var(--color-text-primary)]">
|
||||
{provider.name}
|
||||
</h3>
|
||||
{isCurrent && (
|
||||
<div className="inline-flex items-center gap-1 px-2 py-1 bg-[var(--color-success)]/10 text-[var(--color-success)] rounded-md text-xs font-medium">
|
||||
<CheckCircle2 size={12} />
|
||||
当前使用
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="provider-actions">
|
||||
<button
|
||||
className="enable-btn"
|
||||
onClick={() => onSwitch(provider.id)}
|
||||
disabled={isCurrent}
|
||||
>
|
||||
启用
|
||||
</button>
|
||||
<button
|
||||
className="edit-btn"
|
||||
onClick={() => onEdit(provider.id)}
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
className="delete-btn"
|
||||
onClick={() => onDelete(provider.id)}
|
||||
disabled={isCurrent}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
<div className="flex items-center gap-2 text-sm text-[var(--color-text-secondary)]">
|
||||
{provider.websiteUrl ? (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleUrlClick(provider.websiteUrl!);
|
||||
}}
|
||||
className="inline-flex items-center gap-1 hover:text-[var(--color-primary)] transition-colors"
|
||||
title={`访问 ${provider.websiteUrl}`}
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
{provider.websiteUrl}
|
||||
</button>
|
||||
) : (
|
||||
<span className="font-mono" title={apiUrl}>
|
||||
{apiUrl}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
<button
|
||||
onClick={() => onSwitch(provider.id)}
|
||||
disabled={isCurrent}
|
||||
className={`inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
||||
isCurrent
|
||||
? "bg-[var(--color-bg-tertiary)] text-[var(--color-text-tertiary)] cursor-not-allowed"
|
||||
: "bg-[var(--color-primary)] text-white hover:bg-[var(--color-primary-hover)]"
|
||||
}`}
|
||||
>
|
||||
<Play size={14} />
|
||||
{isCurrent ? "使用中" : "启用"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onEdit(provider.id)}
|
||||
className="p-1.5 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-tertiary)] rounded-md transition-colors"
|
||||
title="编辑供应商"
|
||||
>
|
||||
<Edit3 size={16} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onDelete(provider.id)}
|
||||
disabled={isCurrent}
|
||||
className={`p-1.5 rounded-md transition-colors ${
|
||||
isCurrent
|
||||
? "text-[var(--color-text-tertiary)] cursor-not-allowed"
|
||||
: "text-[var(--color-text-secondary)] hover:text-[var(--color-error)] hover:bg-[var(--color-error-light)]"
|
||||
}`}
|
||||
title="删除供应商"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user