Files
app/shadcn-admin/src/features/characters/components/character-dialog.tsx

290 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { toast } from 'sonner';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Textarea } from '@/components/ui/textarea';
import { useCharacterDialog } from './character-dialog-context';
import { Character, characterSchema } from '../data/schema';
import { createCharacter, updateCharacter } from '../data/api';
import { useQueryClient } from '@tanstack/react-query';
import { ImageUpload } from './image-upload';
export function CharacterDialog() {
const { open, setOpen, character } = useCharacterDialog();
const queryClient = useQueryClient();
const isEdit = !!character;
const form = useForm<any>({
resolver: zodResolver(characterSchema),
defaultValues: {
name: '',
is_active: true,
is_locked: false,
sort_order: 0,
avatar_path: '',
description: '',
ai_system_prompt: '',
ai_greeting: '',
ai_personality: {},
ai_voice_config: {},
},
});
useEffect(() => {
if (character) {
form.reset(character);
} else {
form.reset({
name: '',
is_active: true,
is_locked: false,
sort_order: 0,
avatar_path: '',
description: '',
ai_system_prompt: '',
ai_greeting: '',
});
}
}, [character, form, open]);
const onSubmit = async (values: Character) => {
console.log('🚀 表单提交触发');
console.log('📝 表单值:', values);
console.log('✅ 表单验证状态:', form.formState.isValid);
console.log('❌ 表单错误:', form.formState.errors);
try {
// 确保 JSONB 字段有默认值
const sanitizedValues = {
...values,
ai_personality: values.ai_personality || {},
ai_voice_config: values.ai_voice_config || {},
};
console.log('🔧 处理后的数据:', sanitizedValues);
if (isEdit) {
console.log('🔄 更新角色ID:', character.id);
await updateCharacter({ ...sanitizedValues, id: character.id } as any);
toast.success('角色更新成功');
} else {
console.log(' 创建新角色');
await createCharacter(sanitizedValues as any);
toast.success('角色创建成功');
}
setOpen(false);
queryClient.invalidateQueries({ queryKey: ['characters'] });
} catch (error) {
console.error('❌ 保存角色失败,详细错误:', error);
// 改进错误信息显示
const errorMessage = error instanceof Error
? error.message
: JSON.stringify(error);
toast.error(`保存角色失败: ${errorMessage}`);
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className='max-h-[85vh] overflow-y-auto sm:max-w-2xl'>
<DialogHeader>
<DialogTitle>{isEdit ? '编辑角色' : '创建角色'}</DialogTitle>
<DialogDescription>
{isEdit
? '在下方修改角色详情。'
: '填写详情以创建新角色。'}
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form
onSubmit={(e) => {
console.log('📋 Form onSubmit 事件触发', e);
console.log('📊 当前表单状态:', form.formState);
console.log('❌ 表单错误:', form.formState.errors);
console.log('📝 当前表单值:', form.getValues());
form.handleSubmit(
onSubmit,
(errors) => {
console.error('🚫 表单验证失败:', errors);
toast.error('请检查表单输入');
}
)(e);
}}
className='space-y-4'
>
<div className='grid grid-cols-1 gap-4'>
<FormField
control={form.control}
name='name'
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder='角色名称' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name='avatar_path'
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<ImageUpload
value={field.value}
onChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='description'
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Textarea placeholder='完整角色描述' {...field} value={field.value || ''} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className='grid grid-cols-2 gap-4'>
<FormField
control={form.control}
name='sort_order'
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input
type='number'
{...field}
onChange={e => field.onChange(parseInt(e.target.value))}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className='flex gap-4'>
<FormField
control={form.control}
name='is_active'
render={({ field }) => (
<FormItem className='flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4'>
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className='space-y-1 leading-none'>
<FormLabel></FormLabel>
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name='is_locked'
render={({ field }) => (
<FormItem className='flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4'>
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className='space-y-1 leading-none'>
<FormLabel> ()</FormLabel>
</div>
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name='ai_system_prompt'
render={({ field }) => (
<FormItem>
<FormLabel> (System Prompt)</FormLabel>
<FormControl>
<Textarea
placeholder='你是一个乐于助人的助手...'
className='min-h-[100px]'
{...field}
value={field.value || ''}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='ai_greeting'
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder='你好!' {...field} value={field.value || ''} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className='flex justify-end gap-2'>
<Button type='button' variant='outline' onClick={() => setOpen(false)}>
</Button>
<Button
type='submit'
disabled={form.formState.isSubmitting}
onClick={() => console.log('💾 保存按钮被点击')}
>
{form.formState.isSubmitting ? '保存中...' : '保存'}
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
);
}