Files
aurak/web/components/GroupManager.tsx
T
Developer 8686d101cd Initial commit: AuraK人才测评系统基础框架
## 已实现功能
- 题库管理后端API完整实现
- 模板管理页面(Settings-测评模板)
- 评估统计页面
- 人才测评页面(AssessmentView)
- QuestionBank前端服务层

## 技术栈
- 后端: Node.js + NestJS + TypeORM
- 前端: React + TypeScript
- 容器化: Docker Compose

## 已知待完善
- 题库列表页缺少删除按钮
- 题库详情页未实现(题目管理/AI生成/审核)
2026-05-13 21:32:41 +08:00

242 lines
8.5 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { KnowledgeGroup, CreateGroupData, UpdateGroupData } from '../types';
import { knowledgeGroupService } from '../services/knowledgeGroupService';
import { useToast } from '../contexts/ToastContext';
import { useConfirm } from '../contexts/ConfirmContext';
import { useLanguage } from '../contexts/LanguageContext';
import { Folder, Plus, Edit2, Trash2, X } from 'lucide-react';
interface GroupManagerProps {
groups: KnowledgeGroup[];
onGroupsChange: (groups: KnowledgeGroup[]) => void;
}
const DEFAULT_COLORS = [
'#3B82F6', '#10B981', '#F59E0B', '#EF4444',
'#8B5CF6', '#06B6D4', '#84CC16', '#F97316'
];
export const GroupManager: React.FC<GroupManagerProps> = ({ groups, onGroupsChange }) => {
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [editingGroup, setEditingGroup] = useState<KnowledgeGroup | null>(null);
const [formData, setFormData] = useState<CreateGroupData>({
name: '',
description: '',
color: DEFAULT_COLORS[0],
});
const [loading, setLoading] = useState(false);
const { showSuccess, showError } = useToast();
const { confirm } = useConfirm();
const { t } = useLanguage();
const resetForm = () => {
setFormData({
name: '',
description: '',
color: DEFAULT_COLORS[0],
});
};
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name.trim()) return;
setLoading(true);
try {
const newGroup = await knowledgeGroupService.createGroup(formData);
console.log('[GroupManager] Created group:', newGroup);
console.log('[GroupManager] Current groups:', groups);
onGroupsChange([...groups, newGroup]);
setIsCreateModalOpen(false);
resetForm();
showSuccess(t('successNoteCreated')); // Note: Should probably have a more specific translation for group
} catch (error) {
showError(t('createFailed'));
} finally {
setLoading(false);
}
};
const handleUpdate = async (e: React.FormEvent) => {
e.preventDefault();
if (!editingGroup || !formData.name.trim()) return;
setLoading(true);
try {
const updatedGroup = await knowledgeGroupService.updateGroup(editingGroup.id, formData);
onGroupsChange(groups.map(g => g.id === editingGroup.id ? updatedGroup : g));
setEditingGroup(null);
resetForm();
showSuccess(t('successNoteUpdated'));
} catch (error) {
showError(t('updateFailedRetry'));
} finally {
setLoading(false);
}
};
const handleDelete = async (group: KnowledgeGroup) => {
if (!(await confirm(t('confirmDeleteGroup').replace('$1', group.name)))) return;
try {
await knowledgeGroupService.deleteGroup(group.id);
onGroupsChange(groups.filter(g => g.id !== group.id));
showSuccess(t('successNoteDeleted'));
} catch (error) {
showError(t('deleteFailed'));
}
};
const openEditModal = (group: KnowledgeGroup) => {
setEditingGroup(group);
setFormData({
name: group.name,
description: group.description || '',
color: group.color,
});
};
const closeModal = () => {
setIsCreateModalOpen(false);
setEditingGroup(null);
resetForm();
};
const isModalOpen = isCreateModalOpen || editingGroup !== null;
return (
<div className="space-y-4">
{/* Group list */}
<div className="space-y-2">
{groups.map((group) => (
<div
key={group.id}
className="flex items-center justify-between p-3 bg-white rounded-lg border hover:shadow-sm transition-shadow"
>
<div className="flex items-center space-x-3">
<div
className="w-4 h-4 rounded-full"
style={{ backgroundColor: group.color }}
/>
<div>
<div className="font-medium text-gray-900">{group.name}</div>
{group.description && (
<div className="text-sm text-gray-500">{group.description}</div>
)}
<div className="text-xs text-gray-400">
{group.fileCount} files
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => openEditModal(group)}
className="p-1 text-gray-400 hover:text-blue-600 transition-colors"
>
<Edit2 size={16} />
</button>
<button
onClick={() => handleDelete(group)}
className="p-1 text-gray-400 hover:text-red-600 transition-colors"
>
<Trash2 size={16} />
</button>
</div>
</div>
))}
</div>
{/* Create button */}
<button
onClick={() => setIsCreateModalOpen(true)}
className="w-full flex items-center justify-center p-2 border-2 border-dashed border-gray-300 rounded-lg text-gray-500 hover:border-blue-400 hover:text-blue-600 transition-colors"
title={t('createNotebook')}
>
<Plus size={18} />
</button>
{/* Create/Edit modal */}
{isModalOpen && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-md">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">
{editingGroup ? t('editNotebookTitle') : t('createNotebookTitle')}
</h3>
<button
onClick={closeModal}
className="text-gray-400 hover:text-gray-600"
>
<X size={20} />
</button>
</div>
<form onSubmit={editingGroup ? handleUpdate : handleCreate} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t('name')} *
</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={t('namePlaceholder')}
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t('shortDescription')}
</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={t('descPlaceholder')}
rows={3}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Color indicator
</label>
<div className="flex space-x-2">
{DEFAULT_COLORS.map((color) => (
<button
key={color}
type="button"
onClick={() => setFormData({ ...formData, color })}
className={`w-8 h-8 rounded-full border-2 ${formData.color === color ? 'border-gray-400' : 'border-gray-200'
}`}
style={{ backgroundColor: color }}
/>
))}
</div>
</div>
<div className="flex space-x-3 pt-4">
<button
type="button"
onClick={closeModal}
className="flex-1 px-4 py-2 text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
>
{t('cancel')}
</button>
<button
type="submit"
disabled={loading || !formData.name.trim()}
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{loading ? t('saving') : (editingGroup ? t('save') : t('create'))}
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};