290 lines
12 KiB
TypeScript
290 lines
12 KiB
TypeScript
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>
|
||
);
|
||
}
|