mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-05-14 20:08:42 +08:00
:feat 新增模型配置页面和相关功能
- 新增模型配置页面组件和路由 - 实现模型配置表单和相关逻辑- 添加全局配置入口和功能- 优化首页布局和样式- 新增 404 页面组件 - 更新部分组件样式和结构
This commit is contained in:
153
BillNote_frontend/src/components/Form/modelForm/Form.tsx
Normal file
153
BillNote_frontend/src/components/Form/modelForm/Form.tsx
Normal 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;
|
||||
33
BillNote_frontend/src/components/Form/modelForm/Provider.tsx
Normal file
33
BillNote_frontend/src/components/Form/modelForm/Provider.tsx
Normal 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
|
||||
@@ -0,0 +1,6 @@
|
||||
.card {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
.card:hover {
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user