添加供应商编辑功能和密码显示切换
- 为供应商列表添加启用和编辑按钮 - 创建EditProviderModal组件支持编辑供应商信息 - 实现updateProvider API接口 - 为API Key输入框添加密码显示/隐藏功能,使用SVG图标 - 更新预设供应商配置为YesCode和PackyCode - 移除model字段,简化供应商配置
This commit is contained in:
@@ -6,6 +6,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
getCurrentProvider: () => ipcRenderer.invoke('getCurrentProvider'),
|
||||
addProvider: (provider: Provider) => ipcRenderer.invoke('addProvider', provider),
|
||||
deleteProvider: (id: string) => ipcRenderer.invoke('deleteProvider', id),
|
||||
updateProvider: (provider: Provider) => ipcRenderer.invoke('updateProvider', provider),
|
||||
checkStatus: (provider: Provider) => ipcRenderer.invoke('checkStatus', provider),
|
||||
switchProvider: (providerId: string) => ipcRenderer.invoke('switchProvider', providerId),
|
||||
getClaudeCodeConfigPath: () => ipcRenderer.invoke('getClaudeCodeConfigPath')
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'
|
||||
import { Provider, ProviderStatus } from '../shared/types'
|
||||
import ProviderList from './components/ProviderList'
|
||||
import AddProviderModal from './components/AddProviderModal'
|
||||
import EditProviderModal from './components/EditProviderModal'
|
||||
import './App.css'
|
||||
|
||||
function App() {
|
||||
@@ -11,6 +12,7 @@ function App() {
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [configPath, setConfigPath] = useState<string>('')
|
||||
const [editingProviderId, setEditingProviderId] = useState<string | null>(null)
|
||||
|
||||
// 加载供应商列表
|
||||
useEffect(() => {
|
||||
@@ -81,6 +83,18 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditProvider = async (provider: Provider) => {
|
||||
try {
|
||||
await window.electronAPI.updateProvider(provider)
|
||||
await loadProviders()
|
||||
setEditingProviderId(null)
|
||||
alert('保存成功!')
|
||||
} catch (error) {
|
||||
console.error('更新供应商失败:', error)
|
||||
alert('保存失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<header className="app-header">
|
||||
@@ -109,6 +123,7 @@ function App() {
|
||||
statuses={statuses}
|
||||
onSwitch={handleSwitchProvider}
|
||||
onDelete={handleDeleteProvider}
|
||||
onEdit={setEditingProviderId}
|
||||
/>
|
||||
|
||||
{configPath && (
|
||||
@@ -124,6 +139,14 @@ function App() {
|
||||
onClose={() => setIsAddModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{editingProviderId && providers[editingProviderId] && (
|
||||
<EditProviderModal
|
||||
provider={providers[editingProviderId]}
|
||||
onSave={handleEditProvider}
|
||||
onClose={() => setEditingProviderId(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
position: relative;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.modal-content h2 {
|
||||
@@ -79,6 +81,9 @@
|
||||
border-radius: 4px;
|
||||
font-size: 0.95rem;
|
||||
transition: border-color 0.2s;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
@@ -120,3 +125,46 @@
|
||||
.submit-btn:hover {
|
||||
background: #229954;
|
||||
}
|
||||
|
||||
.password-input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.password-input-wrapper input {
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 0.375rem;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #7f8c8d;
|
||||
transition: color 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.password-toggle svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.password-toggle:hover {
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.password-toggle:focus {
|
||||
outline: none;
|
||||
}
|
||||
@@ -11,9 +11,9 @@ const AddProviderModal: React.FC<AddProviderModalProps> = ({ onAdd, onClose }) =
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
apiUrl: '',
|
||||
apiKey: '',
|
||||
model: 'claude-3-opus-20240229'
|
||||
apiKey: ''
|
||||
})
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
@@ -36,14 +36,12 @@ const AddProviderModal: React.FC<AddProviderModalProps> = ({ onAdd, onClose }) =
|
||||
// 预设的供应商配置
|
||||
const presets = [
|
||||
{
|
||||
name: '官方 Anthropic',
|
||||
apiUrl: 'https://api.anthropic.com',
|
||||
model: 'claude-3-opus-20240229'
|
||||
name: 'YesCode',
|
||||
apiUrl: 'https://co.yes.vg'
|
||||
},
|
||||
{
|
||||
name: 'OpenRouter',
|
||||
apiUrl: 'https://openrouter.ai/api/v1',
|
||||
model: 'anthropic/claude-3-opus'
|
||||
name: 'PackyCode',
|
||||
apiUrl: 'https://api.packycode.com'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -51,8 +49,7 @@ const AddProviderModal: React.FC<AddProviderModalProps> = ({ onAdd, onClose }) =
|
||||
setFormData({
|
||||
...formData,
|
||||
name: preset.name,
|
||||
apiUrl: preset.apiUrl,
|
||||
model: preset.model
|
||||
apiUrl: preset.apiUrl
|
||||
})
|
||||
}
|
||||
|
||||
@@ -106,27 +103,36 @@ const AddProviderModal: React.FC<AddProviderModalProps> = ({ onAdd, onClose }) =
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="apiKey">API Key *</label>
|
||||
<input
|
||||
type="password"
|
||||
id="apiKey"
|
||||
name="apiKey"
|
||||
value={formData.apiKey}
|
||||
onChange={handleChange}
|
||||
placeholder="sk-ant-..."
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="model">模型名称</label>
|
||||
<input
|
||||
type="text"
|
||||
id="model"
|
||||
name="model"
|
||||
value={formData.model}
|
||||
onChange={handleChange}
|
||||
placeholder="claude-3-opus-20240229"
|
||||
/>
|
||||
<div className="password-input-wrapper">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
id="apiKey"
|
||||
name="apiKey"
|
||||
value={formData.apiKey}
|
||||
onChange={handleChange}
|
||||
placeholder={formData.name === 'YesCode' ? 'cr_...' : 'sk-...'}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="password-toggle"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
tabIndex={-1}
|
||||
title={showPassword ? "隐藏密码" : "显示密码"}
|
||||
>
|
||||
{showPassword ? (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
|
||||
<line x1="1" y1="1" x2="23" y2="23" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-actions">
|
||||
|
||||
135
src/renderer/components/EditProviderModal.tsx
Normal file
135
src/renderer/components/EditProviderModal.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Provider } from '../../shared/types'
|
||||
import './AddProviderModal.css'
|
||||
|
||||
interface EditProviderModalProps {
|
||||
provider: Provider
|
||||
onSave: (provider: Provider) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const EditProviderModal: React.FC<EditProviderModalProps> = ({ provider, onSave, onClose }) => {
|
||||
const [formData, setFormData] = useState({
|
||||
name: provider.name,
|
||||
apiUrl: provider.apiUrl,
|
||||
apiKey: provider.apiKey
|
||||
})
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setFormData({
|
||||
name: provider.name,
|
||||
apiUrl: provider.apiUrl,
|
||||
apiKey: provider.apiKey
|
||||
})
|
||||
}, [provider])
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
console.log('提交表单,当前数据:', formData)
|
||||
|
||||
if (!formData.name || !formData.apiUrl || !formData.apiKey) {
|
||||
alert('请填写所有必填字段')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('调用 onSave,provider:', provider, 'formData:', formData)
|
||||
onSave({
|
||||
...provider,
|
||||
...formData
|
||||
})
|
||||
}
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target
|
||||
console.log('输入变化:', name, value)
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||
<h2>编辑供应商</h2>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="name">供应商名称 *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={formData.name || ''}
|
||||
onChange={handleChange}
|
||||
placeholder="例如:官方 Anthropic"
|
||||
required
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="apiUrl">API 地址 *</label>
|
||||
<input
|
||||
type="url"
|
||||
id="apiUrl"
|
||||
name="apiUrl"
|
||||
value={formData.apiUrl || ''}
|
||||
onChange={handleChange}
|
||||
placeholder="https://api.anthropic.com"
|
||||
required
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="apiKey">API Key *</label>
|
||||
<div className="password-input-wrapper">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
id="apiKey"
|
||||
name="apiKey"
|
||||
value={formData.apiKey || ''}
|
||||
onChange={handleChange}
|
||||
placeholder={formData.name && formData.name.includes('YesCode') ? 'cr_...' : 'sk-...'}
|
||||
required
|
||||
autoComplete="off"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="password-toggle"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
tabIndex={-1}
|
||||
title={showPassword ? "隐藏密码" : "显示密码"}
|
||||
>
|
||||
{showPassword ? (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
|
||||
<line x1="1" y1="1" x2="23" y2="23" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-actions">
|
||||
<button type="button" className="cancel-btn" onClick={onClose}>
|
||||
取消
|
||||
</button>
|
||||
<button type="submit" className="submit-btn">
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditProviderModal
|
||||
@@ -105,6 +105,43 @@
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.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 {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid #e74c3c;
|
||||
|
||||
@@ -8,6 +8,7 @@ interface ProviderListProps {
|
||||
statuses: Record<string, ProviderStatus>
|
||||
onSwitch: (id: string) => void
|
||||
onDelete: (id: string) => void
|
||||
onEdit: (id: string) => void
|
||||
}
|
||||
|
||||
const ProviderList: React.FC<ProviderListProps> = ({
|
||||
@@ -15,7 +16,8 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
currentProviderId,
|
||||
statuses,
|
||||
onSwitch,
|
||||
onDelete
|
||||
onDelete,
|
||||
onEdit
|
||||
}) => {
|
||||
const formatResponseTime = (time: number) => {
|
||||
if (time < 0) return '-'
|
||||
@@ -77,6 +79,19 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="provider-actions">
|
||||
<button
|
||||
className="enable-btn"
|
||||
onClick={() => onSwitch(provider.id)}
|
||||
disabled={!status?.isOnline || isCurrent}
|
||||
>
|
||||
启用
|
||||
</button>
|
||||
<button
|
||||
className="edit-btn"
|
||||
onClick={() => onEdit(provider.id)}
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
className="delete-btn"
|
||||
onClick={() => onDelete(provider.id)}
|
||||
|
||||
@@ -25,6 +25,7 @@ declare global {
|
||||
getCurrentProvider: () => Promise<string>
|
||||
addProvider: (provider: Provider) => Promise<boolean>
|
||||
deleteProvider: (id: string) => Promise<boolean>
|
||||
updateProvider: (provider: Provider) => Promise<boolean>
|
||||
checkStatus: (provider: Provider) => Promise<ProviderStatus>
|
||||
switchProvider: (providerId: string) => Promise<boolean>
|
||||
getClaudeCodeConfigPath: () => Promise<string>
|
||||
|
||||
Reference in New Issue
Block a user