feat(providers): add notes field for provider management
- Add notes field to Provider model (backend and frontend) - Display notes with higher priority than URL in provider card - Style notes as non-clickable text to differentiate from URLs - Add notes input field in provider form - Add i18n support (zh/en) for notes field
This commit is contained in:
@@ -22,6 +22,9 @@ pub struct Provider {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
#[serde(rename = "sortIndex")]
|
#[serde(rename = "sortIndex")]
|
||||||
pub sort_index: Option<usize>,
|
pub sort_index: Option<usize>,
|
||||||
|
/// 备注信息
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub notes: Option<String>,
|
||||||
/// 供应商元数据(不写入 live 配置,仅存于 ~/.cc-switch/config.json)
|
/// 供应商元数据(不写入 live 配置,仅存于 ~/.cc-switch/config.json)
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub meta: Option<ProviderMeta>,
|
pub meta: Option<ProviderMeta>,
|
||||||
@@ -43,6 +46,7 @@ impl Provider {
|
|||||||
category: None,
|
category: None,
|
||||||
created_at: None,
|
created_at: None,
|
||||||
sort_index: None,
|
sort_index: None,
|
||||||
|
notes: None,
|
||||||
meta: None,
|
meta: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,10 +33,17 @@ interface ProviderCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const extractApiUrl = (provider: Provider, fallbackText: string) => {
|
const extractApiUrl = (provider: Provider, fallbackText: string) => {
|
||||||
|
// 优先级 1: 备注
|
||||||
|
if (provider.notes?.trim()) {
|
||||||
|
return provider.notes.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先级 2: 官网地址
|
||||||
if (provider.websiteUrl) {
|
if (provider.websiteUrl) {
|
||||||
return provider.websiteUrl;
|
return provider.websiteUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 优先级 3: 从配置中提取请求地址
|
||||||
const config = provider.settingsConfig;
|
const config = provider.settingsConfig;
|
||||||
|
|
||||||
if (config && typeof config === "object") {
|
if (config && typeof config === "object") {
|
||||||
@@ -83,10 +90,24 @@ export function ProviderCard({
|
|||||||
return extractApiUrl(provider, fallbackUrlText);
|
return extractApiUrl(provider, fallbackUrlText);
|
||||||
}, [provider, fallbackUrlText]);
|
}, [provider, fallbackUrlText]);
|
||||||
|
|
||||||
|
// 判断是否为可点击的 URL(备注不可点击)
|
||||||
|
const isClickableUrl = useMemo(() => {
|
||||||
|
// 如果有备注,则不可点击
|
||||||
|
if (provider.notes?.trim()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 如果显示的是回退文本,也不可点击
|
||||||
|
if (displayUrl === fallbackUrlText) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 其他情况(官网地址或请求地址)可点击
|
||||||
|
return true;
|
||||||
|
}, [provider.notes, displayUrl, fallbackUrlText]);
|
||||||
|
|
||||||
const usageEnabled = provider.meta?.usage_script?.enabled ?? false;
|
const usageEnabled = provider.meta?.usage_script?.enabled ?? false;
|
||||||
|
|
||||||
const handleOpenWebsite = () => {
|
const handleOpenWebsite = () => {
|
||||||
if (!displayUrl || displayUrl === fallbackUrlText) {
|
if (!isClickableUrl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onOpenWebsite(displayUrl);
|
onOpenWebsite(displayUrl);
|
||||||
@@ -174,8 +195,14 @@ export function ProviderCard({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleOpenWebsite}
|
onClick={handleOpenWebsite}
|
||||||
className="inline-flex items-center text-sm text-blue-500 transition-colors hover:underline dark:text-blue-400 max-w-[280px]"
|
className={cn(
|
||||||
|
"inline-flex items-center text-sm max-w-[280px]",
|
||||||
|
isClickableUrl
|
||||||
|
? "text-blue-500 transition-colors hover:underline dark:text-blue-400 cursor-pointer"
|
||||||
|
: "text-muted-foreground cursor-default",
|
||||||
|
)}
|
||||||
title={displayUrl}
|
title={displayUrl}
|
||||||
|
disabled={!isClickableUrl}
|
||||||
>
|
>
|
||||||
<span className="truncate">{displayUrl}</span>
|
<span className="truncate">{displayUrl}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -46,6 +46,20 @@ export function BasicFormFields({ form }: BasicFormFieldsProps) {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="notes"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("provider.notes")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder={t("provider.notesPlaceholder")} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ interface ProviderFormProps {
|
|||||||
initialData?: {
|
initialData?: {
|
||||||
name?: string;
|
name?: string;
|
||||||
websiteUrl?: string;
|
websiteUrl?: string;
|
||||||
|
notes?: string;
|
||||||
settingsConfig?: Record<string, unknown>;
|
settingsConfig?: Record<string, unknown>;
|
||||||
category?: ProviderCategory;
|
category?: ProviderCategory;
|
||||||
meta?: ProviderMeta;
|
meta?: ProviderMeta;
|
||||||
@@ -138,6 +139,7 @@ export function ProviderForm({
|
|||||||
() => ({
|
() => ({
|
||||||
name: initialData?.name ?? "",
|
name: initialData?.name ?? "",
|
||||||
websiteUrl: initialData?.websiteUrl ?? "",
|
websiteUrl: initialData?.websiteUrl ?? "",
|
||||||
|
notes: initialData?.notes ?? "",
|
||||||
settingsConfig: initialData?.settingsConfig
|
settingsConfig: initialData?.settingsConfig
|
||||||
? JSON.stringify(initialData.settingsConfig, null, 2)
|
? JSON.stringify(initialData.settingsConfig, null, 2)
|
||||||
: appId === "codex"
|
: appId === "codex"
|
||||||
|
|||||||
@@ -83,6 +83,8 @@
|
|||||||
"name": "Provider Name",
|
"name": "Provider Name",
|
||||||
"namePlaceholder": "e.g., Claude Official",
|
"namePlaceholder": "e.g., Claude Official",
|
||||||
"websiteUrl": "Website URL",
|
"websiteUrl": "Website URL",
|
||||||
|
"notes": "Notes",
|
||||||
|
"notesPlaceholder": "e.g., Company dedicated account",
|
||||||
"configJson": "Config JSON",
|
"configJson": "Config JSON",
|
||||||
"writeCommonConfig": "Write common config",
|
"writeCommonConfig": "Write common config",
|
||||||
"editCommonConfigButton": "Edit common config",
|
"editCommonConfigButton": "Edit common config",
|
||||||
|
|||||||
@@ -83,6 +83,8 @@
|
|||||||
"name": "供应商名称",
|
"name": "供应商名称",
|
||||||
"namePlaceholder": "例如:Claude 官方",
|
"namePlaceholder": "例如:Claude 官方",
|
||||||
"websiteUrl": "官网链接",
|
"websiteUrl": "官网链接",
|
||||||
|
"notes": "备注",
|
||||||
|
"notesPlaceholder": "例如:公司专用账号",
|
||||||
"configJson": "配置 JSON",
|
"configJson": "配置 JSON",
|
||||||
"writeCommonConfig": "写入通用配置",
|
"writeCommonConfig": "写入通用配置",
|
||||||
"editCommonConfigButton": "编辑通用配置",
|
"editCommonConfigButton": "编辑通用配置",
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ function parseJsonError(error: unknown): string {
|
|||||||
export const providerSchema = z.object({
|
export const providerSchema = z.object({
|
||||||
name: z.string().min(1, "请填写供应商名称"),
|
name: z.string().min(1, "请填写供应商名称"),
|
||||||
websiteUrl: z.string().url("请输入有效的网址").optional().or(z.literal("")),
|
websiteUrl: z.string().url("请输入有效的网址").optional().or(z.literal("")),
|
||||||
|
notes: z.string().optional(),
|
||||||
settingsConfig: z
|
settingsConfig: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "请填写配置内容")
|
.min(1, "请填写配置内容")
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export interface Provider {
|
|||||||
category?: ProviderCategory;
|
category?: ProviderCategory;
|
||||||
createdAt?: number; // 添加时间戳(毫秒)
|
createdAt?: number; // 添加时间戳(毫秒)
|
||||||
sortIndex?: number; // 排序索引(用于自定义拖拽排序)
|
sortIndex?: number; // 排序索引(用于自定义拖拽排序)
|
||||||
|
// 备注信息
|
||||||
|
notes?: string;
|
||||||
// 新增:是否为商业合作伙伴
|
// 新增:是否为商业合作伙伴
|
||||||
isPartner?: boolean;
|
isPartner?: boolean;
|
||||||
// 可选:供应商元数据(仅存于 ~/.cc-switch/config.json,不写入 live 配置)
|
// 可选:供应商元数据(仅存于 ~/.cc-switch/config.json,不写入 live 配置)
|
||||||
|
|||||||
Reference in New Issue
Block a user