:feat 新增模型配置页面和相关功能

- 新增模型配置页面组件和路由
- 实现模型配置表单和相关逻辑- 添加全局配置入口和功能- 优化首页布局和样式- 新增 404 页面组件
- 更新部分组件样式和结构
This commit is contained in:
Jefferyhcool
2025-04-22 17:01:02 +08:00
parent 2aad103a77
commit bb974b0b89
95 changed files with 7723 additions and 1697 deletions

View File

@@ -0,0 +1,153 @@
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import {
Form,
FormField,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { useParams } from 'react-router-dom';
import { useProviderStore } from '@/store/providerStore';
import {useEffect, useState} from 'react';
// ✅ 表单校验 schema
const ProviderSchema = z.object({
name: z.string().min(2, '名称不能少于 2 个字符'),
apiKey: z.string().optional(),
baseUrl: z.string().url('必须是合法 URL'),
type: z.string(), // 只展示,不可改
});
type ProviderFormValues = z.infer<typeof ProviderSchema>;
const ProviderForm = () => {
const rawId= useParams();
console.log('rawId',rawId)
// @ts-ignore
const [providerName, idPart] = rawId.id.split('&');
const [id,setId ]= useState(Number(idPart?.split('=')[1])) // => "1"
const getProviderById = useProviderStore((state) => state.getProviderById);
const provider = getProviderById(id);
const form = useForm<ProviderFormValues>({
resolver: zodResolver(ProviderSchema),
defaultValues: {
name: '',
apiKey: '',
baseUrl: '',
type: '',
},
});
useEffect(() => {
console.log(provider)
// if (provider) {
// form.reset({
// name: provider.name,
// apiKey: provider.apiKey,
// baseUrl: provider.baseUrl,
// type: provider.type,
// });
// }
}, [id,provider, form]);
const isBuiltIn = provider?.type === 'built-in';
const onSubmit = (values: ProviderFormValues) => {
console.log('📝 提交表单数据:', values);
// TODO: 提交接口 /update_provider
};
// if (!provider) return <div className="p-4">加载中...</div>;
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="w-full max-w-xl p-4 flex flex-col gap-4"
>
<div className="text-lg font-bold"></div>
{/* 名称 */}
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem className="flex items-center gap-4">
<FormLabel className="w-24 text-right"></FormLabel>
<FormControl>
<Input {...field} disabled={isBuiltIn} className="flex-1" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* API Key */}
<FormField
control={form.control}
name="apiKey"
render={({ field }) => (
<FormItem className="flex items-center gap-4">
<FormLabel className="w-24 text-right">API Key</FormLabel>
<FormControl>
<Input placeholder={'sk-xxx'} {...field} className="flex-1" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Base URL */}
<FormField
control={form.control}
name="baseUrl"
render={({ field }) => (
<FormItem className="flex items-center gap-4">
<FormLabel className="w-24 text-right">API </FormLabel>
<FormControl>
<Input {...field} className="flex-1" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* 类型 */}
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem className="flex items-center gap-4">
<FormLabel className="w-24 text-right"></FormLabel>
<FormControl>
<Input {...field} disabled className="flex-1" />
</FormControl>
</FormItem>
)}
/>
<div className="pt-2">
<Button type="submit" disabled={!form.formState.isDirty}>
</Button>
</div>
</form>
</Form>
);
};
export default ProviderForm;

View File

@@ -0,0 +1,33 @@
import ProviderCard from '@/components/Form/modelForm/components/providerCard.tsx'
import { Button } from '@/components/ui/button.tsx'
import { useProviderStore } from '@/store/providerStore'
const Provider = () => {
const providers = useProviderStore(state => state.provider)
return (
<div className="flex flex-col gap-2">
<div className={'search flex gap-1 py-1.5'}>
<Button type={'button'} className="w-full">
</Button>
</div>
<div className="text-sm font-light"></div>
<div>
{providers &&
providers.map((provider, index) => {
return (
<ProviderCard
key={index}
providerName={provider.name}
Icon={provider.logo}
id={provider.id}
/>
)
})}
</div>
</div>
)
}
export default Provider

View File

@@ -0,0 +1,6 @@
.card {
transition: all 0.2s ease-in-out;
}
.card:hover {
background-color: #f7f7f7;
}

View File

@@ -0,0 +1,43 @@
import { Switch } from '@/components/ui/switch'
import { FC } from 'react'
import styles from './index.module.css'
import {useNavigate, useParams} from 'react-router-dom'
import AILogo from "@/components/Icons";
export interface IProviderCardProps {
id: string
providerName: string
Icon: string
}
const ProviderCard: FC<IProviderCardProps> = ({ providerName, Icon, id }: IProviderCardProps) => {
const navigate = useNavigate()
const handleClick = () => {
navigate(`/settings/model/${providerName}&id=${id}`)
}
const rawId= useParams();
console.log('rawId',rawId)
// @ts-ignore
const { id: currentId } = useParams();
const isActive = currentId === id
return (
<div
onClick={() => {
handleClick()
}}
className={
styles.card + ' flex h-14 items-center justify-between rounded border border-[#f3f3f3] p-2'
+(isActive ? ' bg-[#F0F0F0] font-semibold text-blue-600' : '')
}
>
<div className="flex items-center text-lg">
<div className="h-9 w-9 flex items-center">
<AILogo name={Icon} />
</div>
<div className="font-semibold">{providerName}</div>
</div>
<div>
<Switch />
</div>
</div>
)
}
export default ProviderCard