Files
aurak/web/components/drawers/ImportTasksDrawer.tsx
Developer 0b0a060967 fix: 全部TS错误修复(25->0) + 证书API 500修复 + i18n缺失key补全 + 类型定义修正
- 证书API 500修复: AssessmentCertificate实体注册到app.module.ts
- 前端TS错误25个清零: i18n key 17个, 类型定义8个
- i18n补全: 17个缺失key添加到zh/en/ja
- KnowledgeFile类型: 添加title, content字段
- importService: 改用apiClient.request替代raw fetch
- ModeSelector: 移除jsx prop
- questionBankService: .ok -> .status >= 400
- NotebookDetailView: .filter -> .items.filter
- ImportTasksDrawer: tasks.items提取
- API端点审计: 16/16通过
- 数据库Schema审计: 25表288列一致
- AGENTS.md更新
2026-05-18 08:30:59 +08:00

176 lines
9.4 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { X, Box, Loader2, Trash2 } from 'lucide-react';
import { importService, ImportTask } from '../../services/importService';
import { useLanguage } from '../../contexts/LanguageContext';
import { knowledgeGroupService } from '../../services/knowledgeGroupService';
import { KnowledgeGroup } from '../../types';
interface ImportTasksDrawerProps {
isOpen: boolean;
onClose: () => void;
authToken: string;
}
export const ImportTasksDrawer: React.FC<ImportTasksDrawerProps> = ({
isOpen,
onClose,
authToken,
}) => {
const { t } = useLanguage();
const [importTasks, setImportTasks] = useState<ImportTask[]>([]);
const [groups, setGroups] = useState<KnowledgeGroup[]>([]);
const [isLoading, setIsLoading] = useState(false);
const fetchData = async () => {
if (!authToken) return;
try {
setIsLoading(true);
const [tasks, groupsData] = await Promise.all([
importService.getAll(authToken),
knowledgeGroupService.getGroups()
]);
const taskItems = 'items' in tasks ? tasks.items : tasks;
setImportTasks(taskItems);
// Flatten the groups tree so we can easily find names by ID
const flat: KnowledgeGroup[] = [];
const walk = (items: KnowledgeGroup[]) => {
for (const g of items) {
flat.push(g);
if (g.children?.length) walk(g.children);
}
};
if (groupsData) walk(groupsData);
setGroups(flat);
} catch (error) {
console.error('Failed to fetch data:', error);
} finally {
setIsLoading(false);
}
};
const handleDelete = async (taskId: string) => {
if (!window.confirm(t('confirmDeleteTask'))) return;
try {
await importService.delete(authToken, taskId);
fetchData();
} catch (error) {
console.error('Failed to delete task:', error);
alert(t('deleteTaskFailed'));
}
};
useEffect(() => {
if (isOpen) {
fetchData();
}
}, [isOpen, authToken]);
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex justify-end">
<div className="absolute inset-0 bg-black/20 backdrop-blur-sm transition-opacity" onClick={onClose} />
<div className="relative w-full max-w-4xl bg-white shadow-2xl flex flex-col h-full animate-in slide-in-from-right duration-300">
{/* Header */}
<div className="px-6 py-4 border-b border-slate-100 flex items-center justify-between shrink-0 bg-white">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-xl bg-indigo-50 flex items-center justify-center text-indigo-600">
<Box size={16} />
</div>
<h2 className="text-lg font-bold text-slate-800">{t('importTasksTitle')}</h2>
</div>
<div className="flex items-center gap-2">
<button
onClick={fetchData}
className="p-2 text-slate-400 hover:text-indigo-600 hover:bg-slate-50 rounded-lg transition-all"
title={t('refresh')}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
</button>
<button
onClick={onClose}
className="p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-50 rounded-full transition-colors"
>
<X size={20} />
</button>
</div>
</div>
{/* Body */}
<div className="flex-1 overflow-y-auto p-6 bg-slate-50/30">
{isLoading ? (
<div className="p-12 flex justify-center">
<Loader2 className="animate-spin text-slate-300 w-8 h-8" />
</div>
) : importTasks.length === 0 ? (
<div className="p-12 text-center text-slate-400 flex flex-col items-center">
<Box size={48} className="mb-4 opacity-20" />
<span className="text-sm font-bold uppercase tracking-widest">{t('noTasksFound')}</span>
</div>
) : (
<div className="bg-white rounded-2xl border border-slate-200/60 shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead>
<tr className="border-b border-slate-200/60 bg-slate-50/50 text-[10px] font-black uppercase tracking-widest text-slate-500">
<th className="px-6 py-4">{t('sourcePath')}</th>
<th className="px-6 py-4">{t('targetGroup')}</th>
<th className="px-6 py-4">{t('status')}</th>
<th className="px-6 py-4">{t('scheduledAt')}</th>
<th className="px-6 py-4">{t('createdAt')}</th>
<th className="px-6 py-4 text-right">{t('actions')}</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100/80 text-sm">
{importTasks.map(task => (
<tr key={task.id} className="hover:bg-slate-50/50 transition-colors">
<td className="px-6 py-4 text-slate-900 font-medium">
{task.sourcePath}
</td>
<td className="px-6 py-4 text-slate-500">
{groups.find((g: any) => g.id === task.targetGroupId)?.name || task.targetGroupName || task.targetGroupId || '-'}
</td>
<td className="px-6 py-4">
<span className={`px-2 py-1 rounded-md text-xs font-bold ${task.status === 'COMPLETED' ? 'bg-emerald-100 text-emerald-700' :
task.status === 'FAILED' ? 'bg-red-100 text-red-700' :
task.status === 'PROCESSING' ? 'bg-blue-100 text-blue-700' :
'bg-amber-100 text-amber-700'
}`}>
{task.status}
</span>
{task.status === 'FAILED' && task.logs && (
<div className="text-xs text-red-500 mt-1 max-w-xs truncate" title={task.logs}>
{task.logs}
</div>
)}
</td>
<td className="px-6 py-4 text-slate-500">
{task.scheduledAt ? new Date(task.scheduledAt).toLocaleString() : '-'}
</td>
<td className="px-6 py-4 text-slate-400 text-xs">
{new Date(task.createdAt).toLocaleString()}
</td>
<td className="px-6 py-4 text-right">
<button
onClick={() => handleDelete(task.id)}
className="p-1.5 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
title={t('delete')}
>
<Trash2 size={16} />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
</div>
</div>
);
};