增强供应商配置:添加网站地址字段和智能推测功能

- 添加websiteUrl可选字段到Provider类型
- 实现API地址到网站地址的自动推测逻辑(去除api.前缀)
- 在添加/编辑供应商表单中增加网站地址字段
- 供应商列表智能显示:有网址显示可点击链接,无网址显示API地址
- 提升用户体验:避免点击API端点地址导致的错误页面
This commit is contained in:
farion1231
2025-08-06 10:09:58 +08:00
parent 4540ad613f
commit 71a8fd166f
7 changed files with 126 additions and 21 deletions

View File

@@ -168,3 +168,11 @@
.password-toggle:focus {
outline: none;
}
.field-hint {
display: block;
margin-top: 0.25rem;
color: #7f8c8d;
font-size: 0.8rem;
line-height: 1.3;
}

View File

@@ -1,5 +1,6 @@
import React, { useState } from 'react'
import { Provider } from '../../shared/types'
import { inferWebsiteUrl } from '../../shared/utils'
import './AddProviderModal.css'
interface AddProviderModalProps {
@@ -11,7 +12,8 @@ const AddProviderModal: React.FC<AddProviderModalProps> = ({ onAdd, onClose }) =
const [formData, setFormData] = useState({
name: '',
apiUrl: '',
apiKey: ''
apiKey: '',
websiteUrl: ''
})
const [showPassword, setShowPassword] = useState(false)
@@ -27,10 +29,18 @@ const AddProviderModal: React.FC<AddProviderModalProps> = ({ onAdd, onClose }) =
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
setFormData({
const { name, value } = e.target
const newFormData = {
...formData,
[e.target.name]: e.target.value
})
[name]: value
}
// 如果修改的是API地址自动推测网站地址
if (name === 'apiUrl') {
newFormData.websiteUrl = inferWebsiteUrl(value)
}
setFormData(newFormData)
}
// 预设的供应商配置
@@ -46,11 +56,14 @@ const AddProviderModal: React.FC<AddProviderModalProps> = ({ onAdd, onClose }) =
]
const applyPreset = (preset: typeof presets[0]) => {
setFormData({
const newFormData = {
...formData,
name: preset.name,
apiUrl: preset.apiUrl
})
}
// 应用预设时也自动推测网站地址
newFormData.websiteUrl = inferWebsiteUrl(preset.apiUrl)
setFormData(newFormData)
}
return (
@@ -101,6 +114,19 @@ const AddProviderModal: React.FC<AddProviderModalProps> = ({ onAdd, onClose }) =
/>
</div>
<div className="form-group">
<label htmlFor="websiteUrl"></label>
<input
type="url"
id="websiteUrl"
name="websiteUrl"
value={formData.websiteUrl}
onChange={handleChange}
placeholder="https://example.com可选"
/>
<small className="field-hint">访API地址</small>
</div>
<div className="form-group">
<label htmlFor="apiKey">API Key *</label>
<div className="password-input-wrapper">

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react'
import { Provider } from '../../shared/types'
import { inferWebsiteUrl } from '../../shared/utils'
import './AddProviderModal.css'
interface EditProviderModalProps {
@@ -12,7 +13,8 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({ provider, onSave,
const [formData, setFormData] = useState({
name: provider.name,
apiUrl: provider.apiUrl,
apiKey: provider.apiKey
apiKey: provider.apiKey,
websiteUrl: provider.websiteUrl || ''
})
const [showPassword, setShowPassword] = useState(false)
@@ -20,7 +22,8 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({ provider, onSave,
setFormData({
name: provider.name,
apiUrl: provider.apiUrl,
apiKey: provider.apiKey
apiKey: provider.apiKey,
websiteUrl: provider.websiteUrl || ''
})
}, [provider])
@@ -40,10 +43,17 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({ provider, onSave,
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target
setFormData(prev => ({
...prev,
const newFormData = {
...formData,
[name]: value
}))
}
// 如果修改的是API地址自动推测网站地址
if (name === 'apiUrl') {
newFormData.websiteUrl = inferWebsiteUrl(value)
}
setFormData(newFormData)
}
return (
@@ -80,6 +90,20 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({ provider, onSave,
/>
</div>
<div className="form-group">
<label htmlFor="websiteUrl"></label>
<input
type="url"
id="websiteUrl"
name="websiteUrl"
value={formData.websiteUrl || ''}
onChange={handleChange}
placeholder="https://example.com可选"
autoComplete="off"
/>
<small className="field-hint">访API地址</small>
</div>
<div className="form-group">
<label htmlFor="apiKey">API Key *</label>
<div className="password-input-wrapper">

View File

@@ -90,6 +90,10 @@
text-decoration: underline;
}
.api-url {
color: #7f8c8d;
}
.provider-status {
display: flex;
align-items: center;

View File

@@ -54,16 +54,23 @@ const ProviderList: React.FC<ProviderListProps> = ({
{isCurrent && <span className="current-badge">使</span>}
</div>
<div className="provider-url">
<a
href="#"
onClick={(e) => {
e.preventDefault()
handleUrlClick(provider.apiUrl)
}}
className="url-link"
>
{provider.apiUrl}
</a>
{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={provider.apiUrl}>
{provider.apiUrl}
</span>
)}
</div>
</div>

View File

@@ -4,6 +4,7 @@ export interface Provider {
apiUrl: string
apiKey: string
model?: string
websiteUrl?: string
}
export interface AppConfig {

35
src/shared/utils.ts Normal file
View File

@@ -0,0 +1,35 @@
/**
* 从API地址推测对应的网站地址
* @param apiUrl API地址
* @returns 推测的网站地址,如果无法推测则返回空字符串
*/
export function inferWebsiteUrl(apiUrl: string): string {
if (!apiUrl || !apiUrl.trim()) {
return ''
}
try {
const url = new URL(apiUrl.trim())
// 如果是localhost或IP地址去掉路径部分
if (url.hostname === 'localhost' || /^\d+\.\d+\.\d+\.\d+$/.test(url.hostname)) {
return `${url.protocol}//${url.host}`
}
// 处理域名去掉api前缀
let hostname = url.hostname
// 去掉 api. 前缀
if (hostname.startsWith('api.')) {
hostname = hostname.substring(4)
}
// 构建推测的网站地址
const port = url.port ? `:${url.port}` : ''
return `${url.protocol}//${hostname}${port}`
} catch (error) {
// URL解析失败返回空字符串
return ''
}
}