| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454 |
- {% extends "base.html" %}
- {% block content %}
- <div class="flex h-screen overflow-hidden" id="knowledge-view">
- <!-- Sidebar -->
- {% include 'partials/sidebar.html' %}
- <!-- Main Content -->
- <div class="flex-1 flex flex-col min-w-0 overflow-hidden bg-gray-900/80 relative">
- <!-- Top Header -->
- <header class="h-16 flex items-center justify-between px-6 z-20 bg-gray-900/80 backdrop-blur-md border-b border-blue-900/30">
- <button class="md:hidden text-gray-300 focus:outline-none" id="open-sidebar">
- <i class="fas fa-bars text-xl"></i>
- </button>
- <h1 class="text-lg md:text-2xl font-bold tech-title truncate ml-2">样本中心管理</h1>
- <div class="flex items-center space-x-4">
- <div class="flex items-center space-x-2">
- <div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white font-bold border border-cyan-400 shadow-md">A</div>
- <span class="hidden md:inline text-sm text-gray-300">{{ current_user.username }}</span>
- </div>
- </div>
- </header>
- <!-- Main Scrollable Area -->
- <main class="flex-1 overflow-y-auto p-4 md:p-6 scroll-smooth">
- <div class="h-full flex flex-col gap-6">
- <!-- Tabs -->
- <div class="bg-gray-800 rounded-lg shadow-lg">
- <div class="flex border-b border-gray-700 px-6 pt-4 gap-6">
- <button class="kb-tab active px-4 py-2 text-sm font-bold rounded-t-lg transition-colors" data-tab="list">
- <i class="fas fa-list mr-2"></i>知识库列表
- </button>
- <button class="kb-tab px-4 py-2 text-sm font-bold rounded-t-lg transition-colors text-gray-400 hover:text-white" data-tab="detail">
- <i class="fas fa-info-circle mr-2"></i>知识库详情
- </button>
- <button class="kb-tab px-4 py-2 text-sm font-bold rounded-t-lg transition-colors text-gray-400 hover:text-white" data-tab="tasks">
- <i class="fas fa-tasks mr-2"></i>入库任务
- </button>
- </div>
- </div>
- <!-- Tab 1: 知识库列表 -->
- <div id="tab-list" class="flex-1 bg-gray-800 p-6 rounded-b-lg shadow-lg flex flex-col min-h-0 tab-content">
- <div class="flex justify-between items-center mb-4">
- <label class="text-gray-400 text-sm font-bold">知识库列表</label>
- <div class="flex gap-2 items-center">
- <input type="text" id="kb-search" class="bg-gray-700 text-white rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-48" placeholder="搜索知识库...">
- <button onclick="loadKnowledgeList()" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded text-sm transition-colors">
- <i class="fas fa-sync-alt"></i> 刷新
- </button>
- </div>
- </div>
- <div class="flex-1 overflow-auto custom-scrollbar">
- <table class="w-full text-sm text-gray-300">
- <thead class="text-xs text-gray-400 uppercase bg-gray-700/50 sticky top-0">
- <tr>
- <th class="px-4 py-3 text-left">ID</th>
- <th class="px-4 py-3 text-left">名称</th>
- <th class="px-4 py-3 text-center">文档数</th>
- <th class="px-4 py-3 text-center">状态</th>
- <th class="px-4 py-3 text-center">创建时间</th>
- <th class="px-4 py-3 text-center">操作</th>
- </tr>
- </thead>
- <tbody id="kb-list-body">
- <tr><td colspan="6" class="text-center text-gray-500 py-10">加载中...</td></tr>
- </tbody>
- </table>
- </div>
- <div class="flex justify-between items-center mt-4 pt-4 border-t border-gray-700">
- <span id="kb-total" class="text-gray-500 text-sm"></span>
- <div class="flex gap-2">
- <button onclick="changePage(-1)" id="btn-prev" class="hidden bg-gray-700 hover:bg-gray-600 text-white px-3 py-1 rounded text-sm">上一页</button>
- <span id="kb-page-info" class="text-gray-400 text-sm px-3 py-1"></span>
- <button onclick="changePage(1)" id="btn-next" class="hidden bg-gray-700 hover:bg-gray-600 text-white px-3 py-1 rounded text-sm">下一页</button>
- </div>
- </div>
- </div>
- <!-- Tab 2: 知识库详情 -->
- <div id="tab-detail" class="flex-1 bg-gray-800 p-6 rounded-b-lg shadow-lg flex flex-col min-h-0 tab-content hidden">
- <div id="kb-detail-content">
- <div class="text-center text-gray-500 py-20">
- <i class="fas fa-info-circle text-4xl mb-4 opacity-50"></i>
- <p>请先从知识库列表中选择查看详情</p>
- </div>
- </div>
- </div>
- <!-- Tab 3: 入库任务 -->
- <div id="tab-tasks" class="flex-1 bg-gray-800 p-6 rounded-b-lg shadow-lg flex flex-col min-h-0 tab-content hidden">
- <div class="flex justify-between items-center mb-4">
- <label class="text-gray-400 text-sm font-bold">批量入库任务</label>
- <button onclick="loadImportTasks()" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded text-sm transition-colors">
- <i class="fas fa-sync-alt"></i> 刷新
- </button>
- </div>
- <div class="flex-1 overflow-auto custom-scrollbar">
- <table class="w-full text-sm text-gray-300">
- <thead class="text-xs text-gray-400 uppercase bg-gray-700/50 sticky top-0">
- <tr>
- <th class="px-4 py-3 text-left">任务号</th>
- <th class="px-4 py-3 text-left">知识库</th>
- <th class="px-4 py-3 text-center">状态</th>
- <th class="px-4 py-3 text-center">进度</th>
- <th class="px-4 py-3 text-center">创建时间</th>
- <th class="px-4 py-3 text-center">操作</th>
- </tr>
- </thead>
- <tbody id="task-list-body">
- <tr><td colspan="6" class="text-center text-gray-500 py-10">暂无任务</td></tr>
- </tbody>
- </table>
- </div>
- </div>
- </div>
- </main>
- </div>
- </div>
- <!-- 入库弹窗 Modal -->
- <div id="import-modal" class="fixed inset-0 bg-black/70 z-50 hidden flex items-center justify-center">
- <div class="bg-gray-800 rounded-lg shadow-2xl w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto">
- <div class="flex justify-between items-center p-6 border-b border-gray-700">
- <h2 class="text-xl font-bold text-white"><i class="fas fa-upload mr-2 text-blue-500"></i>批量入库</h2>
- <button onclick="closeImportModal()" class="text-gray-400 hover:text-white"><i class="fas fa-times text-xl"></i></button>
- </div>
- <div class="p-6 space-y-4">
- <div>
- <label class="block text-gray-400 text-sm mb-1 font-bold">知识库</label>
- <input type="text" id="import-kb-name" class="w-full bg-gray-700 text-white rounded p-3 focus:outline-none focus:ring-2 focus:ring-blue-500" readonly>
- <input type="hidden" id="import-kb-id">
- </div>
- <div>
- <label class="block text-gray-400 text-sm mb-1 font-bold">任务号 (task_no)</label>
- <input type="text" id="import-task-no" class="w-full bg-gray-700 text-white rounded p-3 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="自动生成或手动输入">
- </div>
- <div>
- <label class="block text-gray-400 text-sm mb-1 font-bold">Parents (JSON 数组)</label>
- <textarea id="import-parents" class="w-full bg-gray-700 text-white rounded p-3 focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-xs" rows="8" placeholder='[{"index": 0, "parent_id": "...", "text": "..."}]'></textarea>
- </div>
- <div>
- <label class="block text-gray-400 text-sm mb-1 font-bold">Children (JSON 数组,可选)</label>
- <textarea id="import-children" class="w-full bg-gray-700 text-white rounded p-3 focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-xs" rows="6" placeholder='[{"index": 0, "parent_id": "...", "text": "..."}]'></textarea>
- </div>
- </div>
- <div class="flex justify-end gap-3 p-6 border-t border-gray-700">
- <button onclick="closeImportModal()" class="bg-gray-600 hover:bg-gray-500 text-white px-6 py-2 rounded transition-colors">取消</button>
- <button onclick="submitImport()" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded transition-colors"><i class="fas fa-paper-plane mr-1"></i>提交</button>
- </div>
- </div>
- </div>
- <style>
- .kb-tab.active {
- background-color: #1F2937;
- color: white;
- border-bottom: 2px solid #3B82F6;
- }
- .status-pending { color: #F59E0B; }
- .status-processing { color: #3B82F6; }
- .status-success, .status-completed { color: #10B981; }
- .status-failed { color: #EF4444; }
- .custom-scrollbar::-webkit-scrollbar { width: 6px; }
- .custom-scrollbar::-webkit-scrollbar-track { background: #1F2937; }
- .custom-scrollbar::-webkit-scrollbar-thumb { background: #4B5563; border-radius: 3px; }
- </style>
- <script>
- let currentPage = 1;
- let currentKbId = '';
- $(document).ready(function() {
- $('#open-sidebar').click(function() {
- $('.sidebar').toggleClass('-translate-x-full');
- });
- loadKnowledgeList();
- loadImportTasks();
- // Tab switching
- $('.kb-tab').click(function() {
- $('.kb-tab').removeClass('active').addClass('text-gray-400');
- $(this).addClass('active').removeClass('text-gray-400');
- $('.tab-content').addClass('hidden');
- $('#tab-' + $(this).data('tab')).removeClass('hidden');
- });
- // Search
- $('#kb-search').on('keyup', function(e) {
- if (e.key === 'Enter') {
- currentPage = 1;
- loadKnowledgeList();
- }
- });
- // Auto-generate task_no
- $('#import-task-no').on('focus', function() {
- if (!$(this).val()) {
- $(this).val('KIT-' + new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14) + '-' + Math.random().toString(36).slice(2, 6));
- }
- });
- });
- function loadKnowledgeList() {
- const keyword = $('#kb-search').val().trim();
- $.ajax({
- url: '/api/v1/knowledge/bases',
- method: 'GET',
- data: { page: currentPage, page_size: 20 },
- headers: { 'Authorization': 'Bearer ' + getJwtToken() },
- success: function(resp) {
- renderKnowledgeList(resp.data || resp);
- },
- error: function(xhr) {
- $('#kb-list-body').html('<tr><td colspan="6" class="text-center text-red-400 py-10">加载失败: ' + (xhr.responseJSON?.message || '未知错误') + '</td></tr>');
- }
- });
- }
- function renderKnowledgeList(data) {
- const items = data.items || data;
- const total = data.total || items.length;
- const tbody = $('#kb-list-body');
- tbody.empty();
- if (!items || items.length === 0) {
- tbody.html('<tr><td colspan="6" class="text-center text-gray-500 py-10">暂无知识库数据</td></tr>');
- $('#kb-total').text('');
- return;
- }
- items.forEach(function(item) {
- const statusHtml = item.status === 1
- ? '<span class="text-green-400"><i class="fas fa-check-circle"></i> 启用</span>'
- : '<span class="text-red-400"><i class="fas fa-times-circle"></i> 禁用</span>';
- const html = `<tr class="border-b border-gray-700 hover:bg-gray-700/30 transition-colors">
- <td class="px-4 py-3 text-gray-400 font-mono text-xs">${item.id}</td>
- <td class="px-4 py-3 text-white font-medium">${item.name}</td>
- <td class="px-4 py-3 text-center">${item.document_count || 0}</td>
- <td class="px-4 py-3 text-center">${statusHtml}</td>
- <td class="px-4 py-3 text-center text-gray-400 text-xs">${item.created_at || ''}</td>
- <td class="px-4 py-3 text-center">
- <button onclick="viewKbDetail('${item.id}', '${item.name.replace(/'/g, "\\'")}')" class="text-blue-400 hover:text-blue-300 text-xs mr-3"><i class="fas fa-eye"></i> 详情</button>
- <button onclick="openImportModal('${item.id}', '${item.name.replace(/'/g, "\\'")}')" class="text-green-400 hover:text-green-300 text-xs"><i class="fas fa-upload"></i> 入库</button>
- </td>
- </tr>`;
- tbody.append(html);
- });
- const pageSize = data.page_size || 20;
- const page = data.page || 1;
- const totalPages = Math.ceil(total / pageSize);
- $('#kb-total').text('共 ' + total + ' 条');
- $('#kb-page-info').text('第 ' + page + ' / ' + totalPages + ' 页');
- $('#btn-prev').toggleClass('hidden', page <= 1);
- $('#btn-next').toggleClass('hidden', page >= totalPages);
- }
- function changePage(delta) {
- currentPage += delta;
- if (currentPage < 1) currentPage = 1;
- loadKnowledgeList();
- }
- function viewKbDetail(kbId, kbName) {
- // Switch to detail tab
- $('.kb-tab').removeClass('active').addClass('text-gray-400');
- $('.kb-tab[data-tab="detail"]').addClass('active').removeClass('text-gray-400');
- $('.tab-content').addClass('hidden');
- $('#tab-detail').removeClass('hidden');
- $('#kb-detail-content').html('<div class="text-center text-gray-400 py-20"><i class="fas fa-circle-notch fa-spin text-3xl text-blue-500 mb-4"></i><p>加载中...</p></div>');
- $.ajax({
- url: '/api/v1/knowledge/bases/' + kbId,
- method: 'GET',
- headers: { 'Authorization': 'Bearer ' + getJwtToken() },
- success: function(resp) {
- renderKbDetail(resp.data || resp);
- },
- error: function(xhr) {
- $('#kb-detail-content').html('<div class="text-center text-red-400 py-20">加载失败: ' + (xhr.responseJSON?.message || '未知错误') + '</div>');
- }
- });
- }
- function renderKbDetail(data) {
- const schemaHtml = data.metadata_schema && data.metadata_schema.length > 0
- ? `<div class="mt-6"><h3 class="text-white font-bold mb-3"><i class="fas fa-table mr-2 text-blue-500"></i>元数据定义</h3>
- <table class="w-full text-sm text-gray-300">
- <thead class="text-xs text-gray-400 uppercase bg-gray-700/50">
- <tr><th class="px-4 py-2 text-left">中文名</th><th class="px-4 py-2 text-left">英文名</th><th class="px-4 py-2 text-center">类型</th><th class="px-4 py-2 text-left">描述</th></tr>
- </thead>
- <tbody>${data.metadata_schema.map(function(f) {
- return '<tr class="border-b border-gray-700"><td class="px-4 py-2">' + (f.field_name_cn||'') + '</td><td class="px-4 py-2 font-mono text-xs">' + (f.field_name_en||'') + '</td><td class="px-4 py-2 text-center"><span class="bg-gray-600 px-2 py-0.5 rounded text-xs">' + (f.field_type||'') + '</span></td><td class="px-4 py-2 text-gray-400">' + (f.description||'') + '</td></tr>';
- }).join('')}</tbody>
- </table></div>`
- : '';
- $('#kb-detail-content').html(`
- <div class="bg-gray-700/50 rounded-lg p-6">
- <h2 class="text-xl font-bold text-white mb-4"><i class="fas fa-book mr-2 text-blue-500"></i>${data.name}</h2>
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
- <div><span class="text-gray-400">ID:</span> <span class="text-white font-mono">${data.id}</span></div>
- <div><span class="text-gray-400">文档数:</span> <span class="text-white">${data.document_count || 0}</span></div>
- <div><span class="text-gray-400">父表:</span> <span class="text-white font-mono text-xs">${data.parent_table || '-'}</span></div>
- <div><span class="text-gray-400">子表:</span> <span class="text-white font-mono text-xs">${data.child_table || '-'}</span></div>
- <div><span class="text-gray-400">创建人:</span> <span class="text-white">${data.created_by || '-'}</span></div>
- <div><span class="text-gray-400">创建时间:</span> <span class="text-white">${data.created_at || '-'}</span></div>
- </div>
- ${data.description ? '<p class="mt-4 text-gray-300"><span class="text-gray-400">描述:</span> ' + data.description + '</p>' : ''}
- ${schemaHtml}
- <div class="mt-6 flex gap-3">
- <button onclick="openImportModal('${data.id}', '${data.name.replace(/'/g, "\\'")}')" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded transition-colors"><i class="fas fa-upload mr-1"></i>批量入库</button>
- <button onclick="switchTab('list')" class="bg-gray-600 hover:bg-gray-500 text-white px-6 py-2 rounded transition-colors"><i class="fas fa-arrow-left mr-1"></i>返回列表</button>
- </div>
- </div>
- `);
- }
- function switchTab(name) {
- $('.kb-tab').removeClass('active').addClass('text-gray-400');
- $('.kb-tab[data-tab="' + name + '"]').addClass('active').removeClass('text-gray-400');
- $('.tab-content').addClass('hidden');
- $('#tab-' + name).removeClass('hidden');
- }
- function loadImportTasks() {
- $.ajax({
- url: '/api/v1/knowledge/import-tasks',
- method: 'GET',
- data: { page: 1, page_size: 50 },
- headers: { 'Authorization': 'Bearer ' + getJwtToken() },
- success: function(resp) {
- renderImportTasks(resp.data || resp);
- },
- error: function(xhr) {
- $('#task-list-body').html('<tr><td colspan="6" class="text-center text-red-400 py-10">加载失败</td></tr>');
- }
- });
- }
- function renderImportTasks(data) {
- const items = data.items || [];
- const tbody = $('#task-list-body');
- tbody.empty();
- if (items.length === 0) {
- tbody.html('<tr><td colspan="6" class="text-center text-gray-500 py-10">暂无入库任务</td></tr>');
- return;
- }
- const statusMap = {
- 'pending': '<span class="status-pending"><i class="fas fa-clock"></i> 等待中</span>',
- 'processing': '<span class="status-processing"><i class="fas fa-spinner fa-spin"></i> 处理中</span>',
- 'success': '<span class="status-success"><i class="fas fa-check-circle"></i> 已完成</span>',
- 'completed': '<span class="status-success"><i class="fas fa-check-circle"></i> 已完成</span>',
- 'failed': '<span class="status-failed"><i class="fas fa-times-circle"></i> 失败</span>',
- };
- items.forEach(function(item) {
- const progressHtml = item.progress
- ? item.progress.processed + '/' + item.progress.total
- : '-';
- const html = `<tr class="border-b border-gray-700 hover:bg-gray-700/30 transition-colors">
- <td class="px-4 py-3 font-mono text-xs text-gray-300">${item.task_no}</td>
- <td class="px-4 py-3 text-white">${item.kb_name || item.kb_id}</td>
- <td class="px-4 py-3 text-center">${statusMap[item.status] || item.status}</td>
- <td class="px-4 py-3 text-center text-sm">${progressHtml}</td>
- <td class="px-4 py-3 text-center text-gray-400 text-xs">${item.created_at || ''}</td>
- <td class="px-4 py-3 text-center">
- ${item.status === 'processing' || item.status === 'pending'
- ? '<button onclick="refreshTaskStatus(\'' + item.task_no + '\')" class="text-blue-400 hover:text-blue-300 text-xs"><i class="fas fa-sync-alt"></i> 刷新</button>'
- : (item.error_message ? '<span class="text-red-400 text-xs" title="' + item.error_message + '"><i class="fas fa-exclamation-triangle"></i></span>' : '')}
- </td>
- </tr>`;
- tbody.append(html);
- });
- }
- function refreshTaskStatus(taskNo) {
- $.ajax({
- url: '/api/v1/knowledge/import-tasks/' + taskNo,
- method: 'GET',
- headers: { 'Authorization': 'Bearer ' + getJwtToken() },
- success: function() {
- loadImportTasks();
- }
- });
- }
- function openImportModal(kbId, kbName) {
- $('#import-kb-id').val(kbId);
- $('#import-kb-name').val(kbName);
- $('#import-task-no').val('');
- $('#import-parents').val('');
- $('#import-children').val('');
- $('#import-modal').removeClass('hidden');
- }
- function closeImportModal() {
- $('#import-modal').addClass('hidden');
- }
- function submitImport() {
- const kbId = $('#import-kb-id').val();
- const taskNo = $('#import-task-no').val().trim();
- let parents, children;
- if (!taskNo) { alert('请输入任务号'); return; }
- try {
- parents = JSON.parse($('#import-parents').val());
- } catch(e) { alert('Parents JSON 格式错误: ' + e.message); return; }
- const childrenVal = $('#import-children').val().trim();
- children = childrenVal ? JSON.parse(childrenVal) : null;
- $.ajax({
- url: '/api/v1/knowledge/bases/' + kbId + '/batch-import',
- method: 'POST',
- contentType: 'application/json',
- headers: { 'Authorization': 'Bearer ' + getJwtToken() },
- data: JSON.stringify({
- task_no: taskNo,
- kb_name: $('#import-kb-name').val(),
- parents: parents,
- children: children,
- }),
- success: function(resp) {
- alert('入库任务已提交!任务号: ' + taskNo);
- closeImportModal();
- switchTab('tasks');
- loadImportTasks();
- },
- error: function(xhr) {
- alert('提交失败: ' + (xhr.responseJSON?.message || '未知错误'));
- }
- });
- }
- function getJwtToken() {
- // SSO 登录后保存在 localStorage 中的 key 是 'token'
- return localStorage.getItem('token') || localStorage.getItem('jwt_token') || '';
- }
- // 自动刷新任务状态(每 10 秒)
- setInterval(function() {
- if (!$('#tab-tasks').hasClass('hidden')) {
- loadImportTasks();
- }
- }, 10000);
- </script>
- {% endblock %}
|