增强供应商配置:添加网站地址字段和智能推测功能
- 添加websiteUrl可选字段到Provider类型 - 实现API地址到网站地址的自动推测逻辑(去除api.前缀) - 在添加/编辑供应商表单中增加网站地址字段 - 供应商列表智能显示:有网址显示可点击链接,无网址显示API地址 - 提升用户体验:避免点击API端点地址导致的错误页面
This commit is contained in:
@@ -167,4 +167,12 @@
|
|||||||
|
|
||||||
.password-toggle:focus {
|
.password-toggle:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Provider } from '../../shared/types'
|
import { Provider } from '../../shared/types'
|
||||||
|
import { inferWebsiteUrl } from '../../shared/utils'
|
||||||
import './AddProviderModal.css'
|
import './AddProviderModal.css'
|
||||||
|
|
||||||
interface AddProviderModalProps {
|
interface AddProviderModalProps {
|
||||||
@@ -11,7 +12,8 @@ const AddProviderModal: React.FC<AddProviderModalProps> = ({ onAdd, onClose }) =
|
|||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
apiUrl: '',
|
apiUrl: '',
|
||||||
apiKey: ''
|
apiKey: '',
|
||||||
|
websiteUrl: ''
|
||||||
})
|
})
|
||||||
const [showPassword, setShowPassword] = useState(false)
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
|
|
||||||
@@ -27,10 +29,18 @@ const AddProviderModal: React.FC<AddProviderModalProps> = ({ onAdd, onClose }) =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
setFormData({
|
const { name, value } = e.target
|
||||||
|
const newFormData = {
|
||||||
...formData,
|
...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]) => {
|
const applyPreset = (preset: typeof presets[0]) => {
|
||||||
setFormData({
|
const newFormData = {
|
||||||
...formData,
|
...formData,
|
||||||
name: preset.name,
|
name: preset.name,
|
||||||
apiUrl: preset.apiUrl
|
apiUrl: preset.apiUrl
|
||||||
})
|
}
|
||||||
|
// 应用预设时也自动推测网站地址
|
||||||
|
newFormData.websiteUrl = inferWebsiteUrl(preset.apiUrl)
|
||||||
|
setFormData(newFormData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -101,6 +114,19 @@ const AddProviderModal: React.FC<AddProviderModalProps> = ({ onAdd, onClose }) =
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div className="form-group">
|
||||||
<label htmlFor="apiKey">API Key *</label>
|
<label htmlFor="apiKey">API Key *</label>
|
||||||
<div className="password-input-wrapper">
|
<div className="password-input-wrapper">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { Provider } from '../../shared/types'
|
import { Provider } from '../../shared/types'
|
||||||
|
import { inferWebsiteUrl } from '../../shared/utils'
|
||||||
import './AddProviderModal.css'
|
import './AddProviderModal.css'
|
||||||
|
|
||||||
interface EditProviderModalProps {
|
interface EditProviderModalProps {
|
||||||
@@ -12,7 +13,8 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({ provider, onSave,
|
|||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: provider.name,
|
name: provider.name,
|
||||||
apiUrl: provider.apiUrl,
|
apiUrl: provider.apiUrl,
|
||||||
apiKey: provider.apiKey
|
apiKey: provider.apiKey,
|
||||||
|
websiteUrl: provider.websiteUrl || ''
|
||||||
})
|
})
|
||||||
const [showPassword, setShowPassword] = useState(false)
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
|
|
||||||
@@ -20,7 +22,8 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({ provider, onSave,
|
|||||||
setFormData({
|
setFormData({
|
||||||
name: provider.name,
|
name: provider.name,
|
||||||
apiUrl: provider.apiUrl,
|
apiUrl: provider.apiUrl,
|
||||||
apiKey: provider.apiKey
|
apiKey: provider.apiKey,
|
||||||
|
websiteUrl: provider.websiteUrl || ''
|
||||||
})
|
})
|
||||||
}, [provider])
|
}, [provider])
|
||||||
|
|
||||||
@@ -40,10 +43,17 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({ provider, onSave,
|
|||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { name, value } = e.target
|
const { name, value } = e.target
|
||||||
setFormData(prev => ({
|
const newFormData = {
|
||||||
...prev,
|
...formData,
|
||||||
[name]: value
|
[name]: value
|
||||||
}))
|
}
|
||||||
|
|
||||||
|
// 如果修改的是API地址,自动推测网站地址
|
||||||
|
if (name === 'apiUrl') {
|
||||||
|
newFormData.websiteUrl = inferWebsiteUrl(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormData(newFormData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -80,6 +90,20 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({ provider, onSave,
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div className="form-group">
|
||||||
<label htmlFor="apiKey">API Key *</label>
|
<label htmlFor="apiKey">API Key *</label>
|
||||||
<div className="password-input-wrapper">
|
<div className="password-input-wrapper">
|
||||||
|
|||||||
@@ -90,6 +90,10 @@
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.api-url {
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
.provider-status {
|
.provider-status {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -54,16 +54,23 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
{isCurrent && <span className="current-badge">当前使用</span>}
|
{isCurrent && <span className="current-badge">当前使用</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="provider-url">
|
<div className="provider-url">
|
||||||
<a
|
{provider.websiteUrl ? (
|
||||||
href="#"
|
<a
|
||||||
onClick={(e) => {
|
href="#"
|
||||||
e.preventDefault()
|
onClick={(e) => {
|
||||||
handleUrlClick(provider.apiUrl)
|
e.preventDefault()
|
||||||
}}
|
handleUrlClick(provider.websiteUrl!)
|
||||||
className="url-link"
|
}}
|
||||||
>
|
className="url-link"
|
||||||
{provider.apiUrl}
|
title={`访问 ${provider.websiteUrl}`}
|
||||||
</a>
|
>
|
||||||
|
{provider.websiteUrl}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className="api-url" title={provider.apiUrl}>
|
||||||
|
{provider.apiUrl}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface Provider {
|
|||||||
apiUrl: string
|
apiUrl: string
|
||||||
apiKey: string
|
apiKey: string
|
||||||
model?: string
|
model?: string
|
||||||
|
websiteUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
|
|||||||
35
src/shared/utils.ts
Normal file
35
src/shared/utils.ts
Normal 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 ''
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user