first-update
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
TextField,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Box,
|
||||
Chip,
|
||||
Typography,
|
||||
Alert,
|
||||
FormControlLabel,
|
||||
Checkbox
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function TemplateFormDialog({ open, onClose, onSubmit, template }) {
|
||||
const { t } = useTranslation();
|
||||
const [formData, setFormData] = useState({
|
||||
question: '',
|
||||
sourceType: 'text',
|
||||
answerType: 'text',
|
||||
description: '',
|
||||
labels: [],
|
||||
customFormat: '',
|
||||
autoGenerate: true
|
||||
});
|
||||
const [labelInput, setLabelInput] = useState('');
|
||||
const [errors, setErrors] = useState({});
|
||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (template) {
|
||||
setFormData({
|
||||
question: template.question || '',
|
||||
sourceType: template.sourceType || 'text',
|
||||
answerType: template.answerType || 'text',
|
||||
description: template.description || '',
|
||||
labels: template.labels || [],
|
||||
customFormat: template.customFormat ? JSON.stringify(template.customFormat, null, 2) : '',
|
||||
autoGenerate: true // 编辑模式下默认不自动生成
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
question: '',
|
||||
sourceType: 'text',
|
||||
answerType: 'text',
|
||||
description: '',
|
||||
labels: [],
|
||||
customFormat: '',
|
||||
autoGenerate: true
|
||||
});
|
||||
}
|
||||
setErrors({});
|
||||
setShowConfirmDialog(false);
|
||||
}, [template, open]);
|
||||
|
||||
const handleChange = (field, value) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
// 清除该字段的错误
|
||||
if (errors[field]) {
|
||||
setErrors(prev => ({ ...prev, [field]: null }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddLabel = () => {
|
||||
const trimmed = labelInput.trim();
|
||||
if (trimmed && !formData.labels.includes(trimmed)) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
labels: [...prev.labels, trimmed]
|
||||
}));
|
||||
setLabelInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteLabel = labelToDelete => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
labels: prev.labels.filter(label => label !== labelToDelete)
|
||||
}));
|
||||
};
|
||||
|
||||
const validate = () => {
|
||||
const newErrors = {};
|
||||
|
||||
if (!formData.question.trim()) {
|
||||
newErrors.question = t('questions.template.errors.questionRequired');
|
||||
}
|
||||
|
||||
if (formData.answerType === 'label' && formData.labels.length === 0) {
|
||||
newErrors.labels = t('questions.template.errors.labelsRequired');
|
||||
}
|
||||
|
||||
if (formData.answerType === 'custom_format') {
|
||||
if (!formData.customFormat.trim()) {
|
||||
newErrors.customFormat = t('questions.template.errors.customFormatRequired');
|
||||
} else {
|
||||
try {
|
||||
JSON.parse(formData.customFormat);
|
||||
} catch (e) {
|
||||
newErrors.customFormat = t('questions.template.errors.invalidJson');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果选择了自动生成,显示确认对话框
|
||||
if (formData.autoGenerate) {
|
||||
setShowConfirmDialog(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 直接提交
|
||||
submitTemplate();
|
||||
};
|
||||
|
||||
const submitTemplate = () => {
|
||||
const submitData = {
|
||||
question: formData.question.trim(),
|
||||
sourceType: formData.sourceType,
|
||||
answerType: formData.answerType,
|
||||
description: formData.description.trim(),
|
||||
autoGenerate: formData.autoGenerate,
|
||||
templateId: template?.id // 编辑模式时传递模板ID,用于查找未创建问题的数据源
|
||||
};
|
||||
|
||||
if (formData.answerType === 'label') {
|
||||
submitData.labels = formData.labels;
|
||||
}
|
||||
|
||||
if (formData.answerType === 'custom_format') {
|
||||
try {
|
||||
submitData.customFormat = JSON.parse(formData.customFormat);
|
||||
} catch (e) {
|
||||
// 已在验证中处理
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit(submitData);
|
||||
setShowConfirmDialog(false);
|
||||
};
|
||||
|
||||
const handleConfirmGenerate = () => {
|
||||
submitTemplate();
|
||||
};
|
||||
|
||||
const handleCancelGenerate = () => {
|
||||
setShowConfirmDialog(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle>{template ? t('questions.template.edit') : t('questions.template.create')}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{/* 数据源类型 */}
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>{t('questions.template.sourceTypeInfo')}</InputLabel>
|
||||
<Select
|
||||
value={formData.sourceType}
|
||||
label={t('questions.template.sourceTypeInfo')}
|
||||
onChange={e => handleChange('sourceType', e.target.value)}
|
||||
>
|
||||
<MenuItem value="text">{t('questions.template.sourceType.text')}</MenuItem>
|
||||
<MenuItem value="image">{t('questions.template.sourceType.image')}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{/* 问题内容 */}
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('questions.template.question')}
|
||||
value={formData.question}
|
||||
onChange={e => handleChange('question', e.target.value)}
|
||||
error={!!errors.question}
|
||||
helperText={errors.question}
|
||||
required
|
||||
/>
|
||||
|
||||
{/* 答案类型 */}
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>{t('questions.template.answerType.label')}</InputLabel>
|
||||
<Select
|
||||
value={formData.answerType}
|
||||
label={t('questions.template.answerType.label')}
|
||||
onChange={e => handleChange('answerType', e.target.value)}
|
||||
>
|
||||
<MenuItem value="text">{t('questions.template.answerType.text')}</MenuItem>
|
||||
<MenuItem value="label">{t('questions.template.answerType.tags')}</MenuItem>
|
||||
<MenuItem value="custom_format">{t('questions.template.answerType.customFormat')}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{/* 描述 */}
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('questions.template.description')}
|
||||
value={formData.description}
|
||||
onChange={e => handleChange('description', e.target.value)}
|
||||
helperText={t('questions.template.descriptionHelp')}
|
||||
multiline
|
||||
rows={2}
|
||||
/>
|
||||
|
||||
{/* 标签输入 (仅当答案类型为 label 时显示) */}
|
||||
{formData.answerType === 'label' && (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', gap: 1, mb: 1 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('questions.template.addLabel')}
|
||||
value={labelInput}
|
||||
onChange={e => setLabelInput(e.target.value)}
|
||||
onKeyPress={e => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleAddLabel();
|
||||
}
|
||||
}}
|
||||
error={!!errors.labels}
|
||||
helperText={errors.labels}
|
||||
/>
|
||||
<Button variant="outlined" onClick={handleAddLabel} sx={{ minWidth: '100px' }}>
|
||||
{t('common.add')}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
{formData.labels.map(label => (
|
||||
<Chip
|
||||
key={label}
|
||||
label={label}
|
||||
onDelete={() => handleDeleteLabel(label)}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 自定义格式输入 (仅当答案类型为 custom_format 时显示) */}
|
||||
{formData.answerType === 'custom_format' && (
|
||||
<Box>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('questions.template.customFormat')}
|
||||
value={formData.customFormat}
|
||||
onChange={e => handleChange('customFormat', e.target.value)}
|
||||
multiline
|
||||
rows={6}
|
||||
error={!!errors.customFormat}
|
||||
helperText={errors.customFormat || t('questions.template.customFormatHelp')}
|
||||
placeholder='{"field1": "description", "field2": "description"}'
|
||||
/>
|
||||
<Alert severity="info" sx={{ mt: 1 }}>
|
||||
{t('questions.template.customFormatInfo')}
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 自动生成问题选项 */}
|
||||
<Box>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={formData.autoGenerate}
|
||||
onChange={e => handleChange('autoGenerate', e.target.checked)}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={t('questions.template.autoGenerate')}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ ml: 4, mt: 0.5 }}>
|
||||
{formData.sourceType === 'text'
|
||||
? t('questions.template.autoGenerateHelpText')
|
||||
: t('questions.template.autoGenerateHelpImage')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>{t('common.cancel')}</Button>
|
||||
<Button onClick={handleSubmit} variant="contained">
|
||||
{template ? t('common.save') : t('common.create')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
{/* 自动生成确认对话框 */}
|
||||
<Dialog open={showConfirmDialog} onClose={handleCancelGenerate}>
|
||||
<DialogTitle>{t('questions.template.confirmAutoGenerate')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
{template
|
||||
? formData.sourceType === 'text'
|
||||
? t('questions.template.confirmAutoGenerateEditTextMessage', {
|
||||
defaultValue: '您选择了自动生成问题。系统将为所有还未创建此模板问题的文本块创建问题。'
|
||||
})
|
||||
: t('questions.template.confirmAutoGenerateEditImageMessage', {
|
||||
defaultValue: '您选择了自动生成问题。系统将为所有还未创建此模板问题的图片创建问题。'
|
||||
})
|
||||
: formData.sourceType === 'text'
|
||||
? t('questions.template.confirmAutoGenerateTextMessage')
|
||||
: t('questions.template.confirmAutoGenerateImageMessage')}
|
||||
</Typography>
|
||||
<Alert severity="warning" sx={{ mt: 2 }}>
|
||||
{t('questions.template.autoGenerateWarning')}
|
||||
</Alert>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCancelGenerate}>{t('common.cancel')}</Button>
|
||||
<Button onClick={handleConfirmGenerate} variant="contained" color="primary">
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Box,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemSecondaryAction,
|
||||
IconButton,
|
||||
Chip,
|
||||
Typography,
|
||||
Alert,
|
||||
Tabs,
|
||||
Tab
|
||||
} from '@mui/material';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import TemplateFormDialog from './TemplateFormDialog';
|
||||
|
||||
export default function TemplateManagementDialog({
|
||||
open,
|
||||
onClose,
|
||||
templates,
|
||||
onCreateTemplate,
|
||||
onUpdateTemplate,
|
||||
onDeleteTemplate,
|
||||
loading
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
const [editingTemplate, setEditingTemplate] = useState(null);
|
||||
const [currentTab, setCurrentTab] = useState(0); // 0: image, 1: text
|
||||
|
||||
const handleCreate = () => {
|
||||
setEditingTemplate(null);
|
||||
setFormOpen(true);
|
||||
};
|
||||
|
||||
const handleEdit = template => {
|
||||
setEditingTemplate(template);
|
||||
setFormOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = async templateId => {
|
||||
const confirmed = window.confirm(t('questions.template.deleteConfirm'));
|
||||
if (confirmed) {
|
||||
await onDeleteTemplate(templateId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormSubmit = async data => {
|
||||
// 根据当前tab添加sourceType
|
||||
const sourceType = currentTab === 0 ? 'image' : 'text';
|
||||
const templateData = { ...data, sourceType };
|
||||
|
||||
if (editingTemplate) {
|
||||
await onUpdateTemplate(editingTemplate.id, templateData);
|
||||
} else {
|
||||
await onCreateTemplate(templateData);
|
||||
}
|
||||
setFormOpen(false);
|
||||
};
|
||||
|
||||
const getAnswerTypeLabel = type => {
|
||||
const labels = {
|
||||
text: t('questions.template.answerType.text'),
|
||||
label: t('questions.template.answerType.tags'),
|
||||
custom_format: t('questions.template.answerType.customFormat')
|
||||
};
|
||||
return labels[type] || type;
|
||||
};
|
||||
|
||||
// 按数据源类型分组模板
|
||||
const imageTemplates = templates.filter(t => t.sourceType === 'image');
|
||||
const textTemplates = templates.filter(t => t.sourceType === 'text');
|
||||
|
||||
const currentTemplates = currentTab === 0 ? imageTemplates : textTemplates;
|
||||
|
||||
const renderTemplateList = templateList => {
|
||||
if (templateList.length === 0) {
|
||||
return <Alert severity="info">{t('questions.template.noTemplates')}</Alert>;
|
||||
}
|
||||
|
||||
return (
|
||||
<List>
|
||||
{templateList.map(template => (
|
||||
<ListItem key={template.id} divider>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography>{template.question}</Typography>
|
||||
<Chip
|
||||
label={getAnswerTypeLabel(template.answerType)}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
{template.usageCount > 0 && (
|
||||
<Chip
|
||||
label={`${t('questions.template.used')} ${template.usageCount}`}
|
||||
size="small"
|
||||
color="default"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
secondary={template.description}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton edge="end" onClick={() => handleEdit(template)} sx={{ mr: 1 }}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton edge="end" onClick={() => handleDelete(template.id)} disabled={template.usageCount > 0}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="h6">{t('questions.template.management')}</Typography>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={handleCreate} size="small">
|
||||
{t('questions.template.create')}
|
||||
</Button>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
|
||||
<Tabs value={currentTab} onChange={(e, newValue) => setCurrentTab(newValue)}>
|
||||
<Tab label={t('questions.template.sourceType.image')} />
|
||||
<Tab label={t('questions.template.sourceType.text')} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
{renderTemplateList(currentTemplates)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>{t('common.close')}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<TemplateFormDialog
|
||||
open={formOpen}
|
||||
onClose={() => setFormOpen(false)}
|
||||
onSubmit={handleFormSubmit}
|
||||
template={editingTemplate}
|
||||
sourceType={currentTab === 0 ? 'image' : 'text'}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user