0b0a060967
- 证书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更新
176 lines
9.4 KiB
TypeScript
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>
|
|
);
|
|
};
|