initial commit

This commit is contained in:
farion1231
2025-08-04 22:16:26 +08:00
commit e0a9c1ab4c
20 changed files with 1127 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
.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: 8px;
padding: 2rem;
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.modal-content h2 {
margin-bottom: 1.5rem;
color: #2c3e50;
}
.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;
}
.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 {
background: #3498db;
color: white;
}
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: #555;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 0.625rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.95rem;
transition: border-color 0.2s;
}
.form-group input:focus {
outline: none;
border-color: #3498db;
}
.form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
}
.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;
}

View File

@@ -0,0 +1,146 @@
import React, { useState } from 'react'
import { Provider } from '../../shared/types'
import './AddProviderModal.css'
interface AddProviderModalProps {
onAdd: (provider: Omit<Provider, 'id'>) => void
onClose: () => void
}
const AddProviderModal: React.FC<AddProviderModalProps> = ({ onAdd, onClose }) => {
const [formData, setFormData] = useState({
name: '',
apiUrl: '',
apiKey: '',
model: 'claude-3-opus-20240229'
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!formData.name || !formData.apiUrl || !formData.apiKey) {
alert('请填写所有必填字段')
return
}
onAdd(formData)
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
// 预设的供应商配置
const presets = [
{
name: '官方 Anthropic',
apiUrl: 'https://api.anthropic.com',
model: 'claude-3-opus-20240229'
},
{
name: 'OpenRouter',
apiUrl: 'https://openrouter.ai/api/v1',
model: 'anthropic/claude-3-opus'
}
]
const applyPreset = (preset: typeof presets[0]) => {
setFormData({
...formData,
name: preset.name,
apiUrl: preset.apiUrl,
model: preset.model
})
}
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h2></h2>
<div className="presets">
<label></label>
<div className="preset-buttons">
{presets.map((preset, index) => (
<button
key={index}
type="button"
className="preset-btn"
onClick={() => applyPreset(preset)}
>
{preset.name}
</button>
))}
</div>
</div>
<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
/>
</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
/>
</div>
<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>
<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 AddProviderModal

View File

@@ -0,0 +1,127 @@
.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;
}
.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;
}
.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;
}

View File

@@ -0,0 +1,97 @@
import React from 'react'
import { Provider, ProviderStatus } from '../../shared/types'
import './ProviderList.css'
interface ProviderListProps {
providers: Record<string, Provider>
currentProviderId: string
statuses: Record<string, ProviderStatus>
onSwitch: (id: string) => void
onDelete: (id: string) => void
}
const ProviderList: React.FC<ProviderListProps> = ({
providers,
currentProviderId,
statuses,
onSwitch,
onDelete
}) => {
const formatResponseTime = (time: number) => {
if (time < 0) return '-'
return `${time}ms`
}
const getStatusIcon = (status?: ProviderStatus) => {
if (!status) return '⏳'
return status.isOnline ? '✅' : '❌'
}
const getStatusText = (status?: ProviderStatus) => {
if (!status) return '检查中...'
if (status.isOnline) return '正常'
return status.error || '连接失败'
}
return (
<div className="provider-list">
{Object.values(providers).length === 0 ? (
<div className="empty-state">
<p></p>
<p>"添加供应商"</p>
</div>
) : (
<div className="provider-items">
{Object.values(providers).map((provider) => {
const status = statuses[provider.id]
const isCurrent = provider.id === currentProviderId
return (
<div
key={provider.id}
className={`provider-item ${isCurrent ? 'current' : ''}`}
>
<div className="provider-info">
<div className="provider-name">
<input
type="radio"
name="provider"
checked={isCurrent}
onChange={() => onSwitch(provider.id)}
disabled={!status?.isOnline}
/>
<span>{provider.name}</span>
{isCurrent && <span className="current-badge">使</span>}
</div>
<div className="provider-url">{provider.apiUrl}</div>
</div>
<div className="provider-status">
<span className="status-icon">{getStatusIcon(status)}</span>
<span className="status-text">{getStatusText(status)}</span>
{status?.isOnline && (
<span className="response-time">
{formatResponseTime(status.responseTime)}
</span>
)}
</div>
<div className="provider-actions">
<button
className="delete-btn"
onClick={() => onDelete(provider.id)}
disabled={isCurrent}
>
</button>
</div>
</div>
)
})}
</div>
)}
</div>
)
}
export default ProviderList