feat(knowledge): add configuration components and refactor general form

feat(hooks): add llm-related hooks and constants
docs: add architecture analysis for reference projects
This commit is contained in:
2025-10-15 16:24:53 +08:00
parent 486815d83e
commit fe8747983e
33 changed files with 2627 additions and 812 deletions

View File

@@ -1,5 +1,5 @@
import React, { useRef } from 'react';
import { type UseFormReturn } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import {
Box,
Typography,
@@ -12,211 +12,138 @@ import {
Avatar,
Button,
IconButton,
CircularProgress,
} from '@mui/material';
import {
PhotoCamera as PhotoCameraIcon,
Delete as DeleteIcon,
Save as SaveIcon,
} from '@mui/icons-material';
export interface BasicFormData {
name: string;
description: string;
permission: string;
avatar?: string;
}
interface GeneralFormProps {
form: UseFormReturn<BasicFormData>;
onSubmit: (data: BasicFormData) => void;
isSubmitting?: boolean;
onCancel?: () => void;
disabled?: boolean;
submitButtonText?: string;
cancelButtonText?: string;
}
function GeneralForm({
form,
onSubmit,
isSubmitting = false,
onCancel,
disabled = false,
submitButtonText = '提交',
cancelButtonText = '取消'
}: GeneralFormProps) {
function GeneralForm() {
const { control, watch, setValue } = useFormContext();
const fileInputRef = useRef<HTMLInputElement>(null);
// 处理头像上传
const handleAvatarUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
// 检查文件类型
if (!file.type.startsWith('image/')) {
alert('请选择图片文件');
return;
}
// 检查文件大小 (限制为 2MB)
if (file.size > 2 * 1024 * 1024) {
alert('图片大小不能超过 2MB');
return;
}
const reader = new FileReader();
reader.onload = (e) => {
const base64String = e.target?.result as string;
form.setValue('avatar', base64String);
setValue('avatar', e.target?.result as string);
};
reader.readAsDataURL(file);
}
};
// 删除头像
const handleAvatarDelete = () => {
form.setValue('avatar', undefined);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
setValue('avatar', undefined);
};
const avatarValue = form.watch('avatar');
const handleAvatarClick = () => {
fileInputRef.current?.click();
};
const avatar = watch('avatar');
return (
<Box>
<Box sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
</Typography>
<Grid container spacing={3}>
{/* 头像上传 */}
<Grid size={12}>
<Typography variant="subtitle2" gutterBottom>
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Grid size={{xs:12, md:6}}>
<Box sx={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 2 }}>
<Avatar
src={avatarValue}
sx={{ width: 80, height: 80 }}
src={avatar}
sx={{ width: 120, height: 120, cursor: 'pointer' }}
onClick={handleAvatarClick}
>
{!avatarValue && form.watch('name')?.charAt(0)?.toUpperCase()}
{!avatar && <PhotoCameraIcon sx={{ fontSize: 40 }} />}
</Avatar>
<Box>
<input
ref={fileInputRef}
type="file"
accept="image/*"
style={{ display: 'none' }}
onChange={handleAvatarUpload}
disabled={disabled}
/>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
variant="outlined"
startIcon={<PhotoCameraIcon />}
onClick={() => fileInputRef.current?.click()}
disabled={disabled}
size="small"
sx={{ mr: 1 }}
startIcon={<PhotoCameraIcon />}
onClick={handleAvatarClick}
>
</Button>
{avatarValue && (
{avatar && (
<IconButton
onClick={handleAvatarDelete}
disabled={disabled}
size="small"
color="error"
onClick={handleAvatarDelete}
>
<DeleteIcon />
</IconButton>
)}
</Box>
<input
ref={fileInputRef}
type="file"
accept="image/*"
style={{ display: 'none' }}
onChange={handleAvatarUpload}
/>
</Box>
<Typography variant="caption" color="text.secondary">
PNGJPG 2MB
</Typography>
</Grid>
{/* 知识库名称 */}
<Grid size={12}>
<TextField
fullWidth
label="知识库名称"
placeholder="请输入知识库名称"
disabled={disabled}
{...form.register('name', {
required: '请输入知识库名称',
minLength: {
value: 2,
message: '知识库名称至少需要2个字符',
},
maxLength: {
value: 50,
message: '知识库名称不能超过50个字符',
},
})}
error={!!form.formState.errors.name}
helperText={form.formState.errors.name?.message?.toString() || '请输入知识库名称'}
/>
</Grid>
{/* 表单字段 */}
<Grid size={{xs:12,md:8}}>
<Grid container spacing={2}>
<Grid size={{xs:12}}>
<Controller
name="name"
control={control}
rules={{ required: '知识库名称不能为空' }}
render={({ field, fieldState: { error } }) => (
<TextField
{...field}
label="知识库名称"
fullWidth
required
error={!!error}
helperText={error?.message}
/>
)}
/>
</Grid>
{/* 描述 */}
<Grid size={12}>
<TextField
fullWidth
multiline
rows={4}
label="描述"
placeholder="请输入知识库描述"
disabled={disabled}
{...form.register('description', {
maxLength: {
value: 500,
message: '描述不能超过500个字符',
},
})}
error={!!form.formState.errors.description}
helperText={form.formState.errors.description?.message?.toString() || '请输入知识库描述'}
/>
</Grid>
<Grid size={{xs:12}}>
<Controller
name="description"
control={control}
render={({ field }) => (
<TextField
{...field}
label="描述"
fullWidth
multiline
rows={3}
placeholder="请输入知识库描述..."
/>
)}
/>
</Grid>
{/* 权限设置 */}
<Grid size={{xs:12,sm:6}}>
<FormControl fullWidth disabled={disabled}>
<InputLabel></InputLabel>
<Select
label="权限设置"
{...form.register('permission')}
value={form.watch('permission') || 'me'}
>
<MenuItem value="me"></MenuItem>
<MenuItem value="team"></MenuItem>
</Select>
</FormControl>
</Grid>
{/* 操作按钮 */}
<Grid size={12}>
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'flex-end', mt: 2 }}>
{onCancel && (
<Button
variant="outlined"
onClick={onCancel}
disabled={isSubmitting}
>
{cancelButtonText}
</Button>
)}
<Button
variant="contained"
onClick={form.handleSubmit(onSubmit)}
disabled={disabled || isSubmitting}
startIcon={isSubmitting ? <CircularProgress size={20} /> : <SaveIcon />}
>
{submitButtonText}
</Button>
</Box>
<Grid size={{xs:12}}>
<Controller
name="permission"
control={control}
render={({ field }) => (
<FormControl fullWidth>
<InputLabel></InputLabel>
<Select {...field} label="权限设置">
<MenuItem value="me"></MenuItem>
<MenuItem value="team"></MenuItem>
</Select>
</FormControl>
)}
/>
</Grid>
</Grid>
</Grid>
</Grid>
</Box>