添加供应商编辑功能和密码显示切换
- 为供应商列表添加启用和编辑按钮 - 创建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'),
|
getCurrentProvider: () => ipcRenderer.invoke('getCurrentProvider'),
|
||||||
addProvider: (provider: Provider) => ipcRenderer.invoke('addProvider', provider),
|
addProvider: (provider: Provider) => ipcRenderer.invoke('addProvider', provider),
|
||||||
deleteProvider: (id: string) => ipcRenderer.invoke('deleteProvider', id),
|
deleteProvider: (id: string) => ipcRenderer.invoke('deleteProvider', id),
|
||||||
|
updateProvider: (provider: Provider) => ipcRenderer.invoke('updateProvider', provider),
|
||||||
checkStatus: (provider: Provider) => ipcRenderer.invoke('checkStatus', provider),
|
checkStatus: (provider: Provider) => ipcRenderer.invoke('checkStatus', provider),
|
||||||
switchProvider: (providerId: string) => ipcRenderer.invoke('switchProvider', providerId),
|
switchProvider: (providerId: string) => ipcRenderer.invoke('switchProvider', providerId),
|
||||||
getClaudeCodeConfigPath: () => ipcRenderer.invoke('getClaudeCodeConfigPath')
|
getClaudeCodeConfigPath: () => ipcRenderer.invoke('getClaudeCodeConfigPath')
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'
|
|||||||
import { Provider, ProviderStatus } from '../shared/types'
|
import { Provider, ProviderStatus } from '../shared/types'
|
||||||
import ProviderList from './components/ProviderList'
|
import ProviderList from './components/ProviderList'
|
||||||
import AddProviderModal from './components/AddProviderModal'
|
import AddProviderModal from './components/AddProviderModal'
|
||||||
|
import EditProviderModal from './components/EditProviderModal'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@@ -11,6 +12,7 @@ function App() {
|
|||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||||
const [configPath, setConfigPath] = useState<string>('')
|
const [configPath, setConfigPath] = useState<string>('')
|
||||||
|
const [editingProviderId, setEditingProviderId] = useState<string | null>(null)
|
||||||
|
|
||||||
// 加载供应商列表
|
// 加载供应商列表
|
||||||
useEffect(() => {
|
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 (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<header className="app-header">
|
<header className="app-header">
|
||||||
@@ -109,6 +123,7 @@ function App() {
|
|||||||
statuses={statuses}
|
statuses={statuses}
|
||||||
onSwitch={handleSwitchProvider}
|
onSwitch={handleSwitchProvider}
|
||||||
onDelete={handleDeleteProvider}
|
onDelete={handleDeleteProvider}
|
||||||
|
onEdit={setEditingProviderId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{configPath && (
|
{configPath && (
|
||||||
@@ -124,6 +139,14 @@ function App() {
|
|||||||
onClose={() => setIsAddModalOpen(false)}
|
onClose={() => setIsAddModalOpen(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{editingProviderId && providers[editingProviderId] && (
|
||||||
|
<EditProviderModal
|
||||||
|
provider={providers[editingProviderId]}
|
||||||
|
onSave={handleEditProvider}
|
||||||
|
onClose={() => setEditingProviderId(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1001;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content h2 {
|
.modal-content h2 {
|
||||||
@@ -79,6 +81,9 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
transition: border-color 0.2s;
|
transition: border-color 0.2s;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
background: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input:focus {
|
.form-group input:focus {
|
||||||
@@ -120,3 +125,46 @@
|
|||||||
.submit-btn:hover {
|
.submit-btn:hover {
|
||||||
background: #229954;
|
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({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
apiUrl: '',
|
apiUrl: '',
|
||||||
apiKey: '',
|
apiKey: ''
|
||||||
model: 'claude-3-opus-20240229'
|
|
||||||
})
|
})
|
||||||
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -36,14 +36,12 @@ const AddProviderModal: React.FC<AddProviderModalProps> = ({ onAdd, onClose }) =
|
|||||||
// 预设的供应商配置
|
// 预设的供应商配置
|
||||||
const presets = [
|
const presets = [
|
||||||
{
|
{
|
||||||
name: '官方 Anthropic',
|
name: 'YesCode',
|
||||||
apiUrl: 'https://api.anthropic.com',
|
apiUrl: 'https://co.yes.vg'
|
||||||
model: 'claude-3-opus-20240229'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'OpenRouter',
|
name: 'PackyCode',
|
||||||
apiUrl: 'https://openrouter.ai/api/v1',
|
apiUrl: 'https://api.packycode.com'
|
||||||
model: 'anthropic/claude-3-opus'
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -51,8 +49,7 @@ const AddProviderModal: React.FC<AddProviderModalProps> = ({ onAdd, onClose }) =
|
|||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
name: preset.name,
|
name: preset.name,
|
||||||
apiUrl: preset.apiUrl,
|
apiUrl: preset.apiUrl
|
||||||
model: preset.model
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,27 +103,36 @@ const AddProviderModal: React.FC<AddProviderModalProps> = ({ onAdd, onClose }) =
|
|||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="apiKey">API Key *</label>
|
<label htmlFor="apiKey">API Key *</label>
|
||||||
<input
|
<div className="password-input-wrapper">
|
||||||
type="password"
|
<input
|
||||||
id="apiKey"
|
type={showPassword ? "text" : "password"}
|
||||||
name="apiKey"
|
id="apiKey"
|
||||||
value={formData.apiKey}
|
name="apiKey"
|
||||||
onChange={handleChange}
|
value={formData.apiKey}
|
||||||
placeholder="sk-ant-..."
|
onChange={handleChange}
|
||||||
required
|
placeholder={formData.name === 'YesCode' ? 'cr_...' : 'sk-...'}
|
||||||
/>
|
required
|
||||||
</div>
|
/>
|
||||||
|
<button
|
||||||
<div className="form-group">
|
type="button"
|
||||||
<label htmlFor="model">模型名称</label>
|
className="password-toggle"
|
||||||
<input
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
type="text"
|
tabIndex={-1}
|
||||||
id="model"
|
title={showPassword ? "隐藏密码" : "显示密码"}
|
||||||
name="model"
|
>
|
||||||
value={formData.model}
|
{showPassword ? (
|
||||||
onChange={handleChange}
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
placeholder="claude-3-opus-20240229"
|
<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>
|
||||||
|
|
||||||
<div className="form-actions">
|
<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;
|
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 {
|
.delete-btn {
|
||||||
padding: 0.375rem 0.75rem;
|
padding: 0.375rem 0.75rem;
|
||||||
border: 1px solid #e74c3c;
|
border: 1px solid #e74c3c;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ interface ProviderListProps {
|
|||||||
statuses: Record<string, ProviderStatus>
|
statuses: Record<string, ProviderStatus>
|
||||||
onSwitch: (id: string) => void
|
onSwitch: (id: string) => void
|
||||||
onDelete: (id: string) => void
|
onDelete: (id: string) => void
|
||||||
|
onEdit: (id: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProviderList: React.FC<ProviderListProps> = ({
|
const ProviderList: React.FC<ProviderListProps> = ({
|
||||||
@@ -15,7 +16,8 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
currentProviderId,
|
currentProviderId,
|
||||||
statuses,
|
statuses,
|
||||||
onSwitch,
|
onSwitch,
|
||||||
onDelete
|
onDelete,
|
||||||
|
onEdit
|
||||||
}) => {
|
}) => {
|
||||||
const formatResponseTime = (time: number) => {
|
const formatResponseTime = (time: number) => {
|
||||||
if (time < 0) return '-'
|
if (time < 0) return '-'
|
||||||
@@ -77,6 +79,19 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="provider-actions">
|
<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
|
<button
|
||||||
className="delete-btn"
|
className="delete-btn"
|
||||||
onClick={() => onDelete(provider.id)}
|
onClick={() => onDelete(provider.id)}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ declare global {
|
|||||||
getCurrentProvider: () => Promise<string>
|
getCurrentProvider: () => Promise<string>
|
||||||
addProvider: (provider: Provider) => Promise<boolean>
|
addProvider: (provider: Provider) => Promise<boolean>
|
||||||
deleteProvider: (id: string) => Promise<boolean>
|
deleteProvider: (id: string) => Promise<boolean>
|
||||||
|
updateProvider: (provider: Provider) => Promise<boolean>
|
||||||
checkStatus: (provider: Provider) => Promise<ProviderStatus>
|
checkStatus: (provider: Provider) => Promise<ProviderStatus>
|
||||||
switchProvider: (providerId: string) => Promise<boolean>
|
switchProvider: (providerId: string) => Promise<boolean>
|
||||||
getClaudeCodeConfigPath: () => Promise<string>
|
getClaudeCodeConfigPath: () => Promise<string>
|
||||||
|
|||||||
Reference in New Issue
Block a user