deep_management.html 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. {% extends "base.html" %}
  2. {% block content %}
  3. <div class="flex h-screen overflow-hidden" id="deep-view">
  4. <!-- Sidebar -->
  5. {% include 'partials/sidebar.html' %}
  6. <!-- Main Content -->
  7. <div class="flex-1 flex flex-col min-w-0 overflow-hidden bg-gray-900/80 relative">
  8. <!-- Top Header -->
  9. <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">
  10. <button class="md:hidden text-gray-300 focus:outline-none" id="open-sidebar">
  11. <i class="fas fa-bars text-xl"></i>
  12. </button>
  13. <h1 class="text-lg md:text-2xl font-bold tech-title truncate ml-2">AI深度采集管理</h1>
  14. <div class="flex items-center space-x-4">
  15. <div class="flex items-center space-x-2">
  16. <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>
  17. <span class="hidden md:inline text-sm text-gray-300">{{ current_user.username }}</span>
  18. </div>
  19. </div>
  20. </header>
  21. <!-- Main Scrollable Area -->
  22. <main class="flex-1 overflow-y-auto p-4 md:p-6 scroll-smooth">
  23. <div class="tech-panel p-6 rounded-xl mb-6 min-h-[calc(100vh-140px)] flex flex-col">
  24. <!-- 1. Search Area -->
  25. <div class="flex flex-col md:flex-row gap-4 mb-6">
  26. <div class="flex-1 flex gap-2">
  27. <input type="text" id="search-keyword" class="flex-1 bg-gray-700 text-white rounded p-2 border border-gray-600 focus:outline-none focus:border-blue-500" placeholder="请输入URL或内容关键字...">
  28. <button onclick="searchData()" class="bg-blue-600 hover:bg-blue-700 text-white px-6 rounded font-medium transition-colors">
  29. <i class="fas fa-search"></i> 搜索
  30. </button>
  31. </div>
  32. </div>
  33. <!-- 2. Toolbar -->
  34. <div class="flex gap-3 mb-4 p-3 bg-gray-800/50 rounded-lg border border-gray-700">
  35. <button onclick="batchDelete()" class="bg-red-600/80 hover:bg-red-600 text-white px-4 py-2 rounded text-sm transition-colors flex items-center gap-2">
  36. <i class="fas fa-trash-alt"></i> 批量删除
  37. </button>
  38. </div>
  39. <!-- 3. Data Table -->
  40. <div class="flex-1 overflow-x-auto">
  41. <table class="w-full text-left border-collapse">
  42. <thead>
  43. <tr class="text-gray-400 border-b border-gray-700 bg-gray-800/30">
  44. <th class="p-4 w-12 text-center">
  45. <input type="checkbox" id="select-all" onclick="toggleSelectAll()" class="rounded bg-gray-700 border-gray-600 text-blue-500 focus:ring-offset-gray-900">
  46. </th>
  47. <th class="p-4 w-20">ID</th>
  48. <th class="p-4">标题 / URL</th>
  49. <th class="p-4 w-32">状态</th>
  50. <th class="p-4 w-64">AI摘要</th>
  51. <th class="p-4 w-48">更新时间</th>
  52. <th class="p-4 w-48 text-center">操作</th>
  53. </tr>
  54. </thead>
  55. <tbody id="data-table-body" class="text-gray-300 divide-y divide-gray-700/50">
  56. <!-- Rows will be injected here -->
  57. </tbody>
  58. </table>
  59. </div>
  60. <!-- 4. Pagination -->
  61. <div class="mt-6 flex justify-between items-center border-t border-gray-700 pt-4">
  62. <div class="text-sm text-gray-500">
  63. 共 <span id="total-count" class="text-white font-bold">0</span> 条数据
  64. </div>
  65. <div class="flex gap-2" id="pagination-controls">
  66. <!-- Pagination buttons -->
  67. </div>
  68. </div>
  69. </div>
  70. </main>
  71. </div>
  72. </div>
  73. <!-- View Modal -->
  74. <div id="view-modal" class="fixed inset-0 z-50 hidden">
  75. <div class="absolute inset-0 bg-black/80 backdrop-blur-sm" onclick="closeViewModal()"></div>
  76. <div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-gray-900 rounded-xl shadow-2xl border border-gray-700 w-[800px] h-[90vh] flex flex-col animate-fade-in-up">
  77. <div class="p-4 border-b border-gray-700 flex justify-between items-center">
  78. <h3 class="text-xl font-bold text-white truncate max-w-[600px]" id="view-url">URL</h3>
  79. <button onclick="closeViewModal()" class="text-gray-400 hover:text-white"><i class="fas fa-times"></i></button>
  80. </div>
  81. <div class="flex-1 overflow-y-auto p-6">
  82. <div class="mb-6">
  83. <h4 class="text-sm font-bold text-purple-400 mb-2 uppercase tracking-wider">AI 摘要</h4>
  84. <div class="bg-gray-800 p-4 rounded-lg text-gray-300 leading-relaxed" id="view-summary"></div>
  85. </div>
  86. <div>
  87. <h4 class="text-sm font-bold text-blue-400 mb-2 uppercase tracking-wider">采集内容 (Markdown)</h4>
  88. <pre class="bg-gray-800 p-4 rounded-lg text-gray-300 overflow-x-auto whitespace-pre-wrap font-mono text-sm" id="view-content"></pre>
  89. </div>
  90. </div>
  91. </div>
  92. </div>
  93. <!-- Toast Notification -->
  94. <div id="toast" class="fixed top-5 left-1/2 transform -translate-x-1/2 z-50 hidden transition-all duration-300">
  95. <div class="bg-gray-800 border-l-4 border-blue-500 text-white px-6 py-3 rounded shadow-lg flex items-center gap-3">
  96. <i class="fas fa-info-circle text-blue-400" id="toast-icon"></i>
  97. <span id="toast-message">Notification</span>
  98. </div>
  99. </div>
  100. <!-- Confirm Modal -->
  101. <div id="confirm-modal" class="fixed inset-0 z-50 hidden">
  102. <div class="absolute inset-0 bg-black/60 backdrop-blur-sm" onclick="closeConfirm()"></div>
  103. <div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-gray-800 rounded-xl shadow-2xl border border-gray-700 w-96 p-6 animate-fade-in-up">
  104. <h3 class="text-xl font-bold text-white mb-2">确认操作</h3>
  105. <p class="text-gray-400 mb-6" id="confirm-message">确定要执行此操作吗?</p>
  106. <div class="flex justify-end gap-3">
  107. <button onclick="closeConfirm()" class="px-4 py-2 rounded text-gray-400 hover:text-white hover:bg-gray-700 transition-colors">取消</button>
  108. <button id="confirm-btn" class="px-4 py-2 rounded bg-red-600 text-white hover:bg-red-700 transition-colors shadow-lg">确定</button>
  109. </div>
  110. </div>
  111. </div>
  112. <!-- Import Modal -->
  113. <div id="import-modal" class="fixed inset-0 z-50 hidden">
  114. <div class="absolute inset-0 bg-black/60 backdrop-blur-sm" onclick="closeImportModal()"></div>
  115. <div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-gray-800 rounded-xl shadow-2xl border border-gray-700 w-[480px] p-6 animate-fade-in-up">
  116. <h3 class="text-xl font-bold text-white mb-1">数据入库</h3>
  117. <p class="text-sm text-gray-400 mb-4" id="import-item-title"></p>
  118. <label class="text-sm text-gray-300 mb-1 block">选择知识库</label>
  119. <select id="import-kb-select" class="w-full bg-gray-700 text-white rounded p-2 border border-gray-600 focus:outline-none focus:border-blue-500 mb-4">
  120. <option value="">加载中...</option>
  121. </select>
  122. <div class="flex justify-end gap-3">
  123. <button onclick="closeImportModal()" class="px-4 py-2 rounded text-gray-400 hover:text-white hover:bg-gray-700 transition-colors">取消</button>
  124. <button id="import-submit-btn" class="px-4 py-2 rounded bg-green-600 text-white hover:bg-green-700 transition-colors shadow-lg">
  125. <i class="fas fa-upload"></i> 提交入库
  126. </button>
  127. </div>
  128. </div>
  129. </div>
  130. <script>
  131. let currentPage = 1;
  132. let currentKeyword = '';
  133. const pageSize = 10;
  134. // Check URL params for initial query
  135. const urlParams = new URLSearchParams(window.location.search);
  136. const initialQuery = urlParams.get('query');
  137. $(document).ready(function() {
  138. if (initialQuery) {
  139. $('#search-keyword').val(initialQuery);
  140. currentKeyword = initialQuery;
  141. }
  142. loadData();
  143. $('#search-keyword').keypress(function(e) {
  144. if(e.which == 13) {
  145. searchData();
  146. }
  147. });
  148. $('#open-sidebar').click(function() {
  149. $('#sidebar').toggleClass('-translate-x-full');
  150. });
  151. });
  152. function searchData() {
  153. currentKeyword = $('#search-keyword').val().trim();
  154. currentPage = 1;
  155. loadData();
  156. }
  157. function loadData() {
  158. const tbody = $('#data-table-body');
  159. tbody.html('<tr><td colspan="7" class="text-center py-10 text-gray-500"><i class="fas fa-spinner fa-spin mr-2"></i>加载中...</td></tr>');
  160. $.get('/deep/api/list', {
  161. page: currentPage,
  162. per_page: pageSize,
  163. query: currentKeyword
  164. }, function(response) {
  165. renderTable(response.items);
  166. renderPagination(response);
  167. $('#total-count').text(response.total);
  168. $('#select-all').prop('checked', false);
  169. }).fail(function() {
  170. tbody.html('<tr><td colspan="7" class="text-center py-10 text-red-500">加载失败,请重试</td></tr>');
  171. });
  172. }
  173. function renderTable(items) {
  174. const tbody = $('#data-table-body');
  175. tbody.empty();
  176. if (items.length === 0) {
  177. tbody.html('<tr><td colspan="7" class="text-center py-10 text-gray-500">暂无数据</td></tr>');
  178. return;
  179. }
  180. items.forEach(item => {
  181. const html = `
  182. <tr class="hover:bg-gray-800/50 transition-colors group">
  183. <td class="p-4 text-center">
  184. <input type="checkbox" class="row-checkbox rounded bg-gray-700 border-gray-600 text-blue-500 focus:ring-offset-gray-900" value="${item.id}">
  185. </td>
  186. <td class="p-4 text-gray-500 text-sm">#${item.id}</td>
  187. <td class="p-4">
  188. <div class="flex flex-col">
  189. <a href="${item.url}" target="_blank" class="text-white font-medium hover:text-blue-400 line-clamp-1 mb-1" title="${item.title || item.url}">
  190. ${item.title || item.url}
  191. </a>
  192. <div class="text-xs text-gray-500 line-clamp-1 font-mono">${item.url}</div>
  193. </div>
  194. </td>
  195. <td class="p-4">
  196. <span class="px-2 py-1 rounded text-xs ${item.status === 'completed' ? 'bg-green-900/50 text-green-400 border border-green-800' : 'bg-red-900/50 text-red-400 border border-red-800'}">
  197. ${item.status === 'completed' ? '已完成' : '失败'}
  198. </span>
  199. </td>
  200. <td class="p-4 text-gray-400 text-sm line-clamp-2" title="${item.summary || ''}">
  201. ${item.summary || '无摘要'}
  202. </td>
  203. <td class="p-4 text-sm text-gray-400 font-mono">${item.updated_at}</td>
  204. <td class="p-4">
  205. <div class="flex justify-center gap-2">
  206. <button onclick="viewDeepItem(${item.id})" class="text-blue-400 hover:text-blue-300 p-1" title="查看详情">
  207. <i class="fas fa-eye"></i>
  208. </button>
  209. <button onclick="openImportModal(${item.id})" class="text-green-400 hover:text-green-300 p-1" title="入库">
  210. <i class="fas fa-upload"></i>
  211. </button>
  212. <button onclick="deleteItem(${item.id})" class="text-red-400 hover:text-red-300 p-1" title="删除">
  213. <i class="fas fa-trash-alt"></i>
  214. </button>
  215. </div>
  216. </td>
  217. </tr>
  218. `;
  219. tbody.append(html);
  220. });
  221. }
  222. function viewDeepItem(id) {
  223. $.get('/deep/api/get/' + id, function(data) {
  224. $('#view-url').text(data.url);
  225. $('#view-summary').text(data.summary || '暂无摘要');
  226. $('#view-content').text(data.content || '暂无内容');
  227. $('#view-modal').removeClass('hidden');
  228. }).fail(function() {
  229. showToast('获取详情失败', 'error');
  230. });
  231. }
  232. function closeViewModal() {
  233. $('#view-modal').addClass('hidden');
  234. }
  235. function deleteItem(id) {
  236. showConfirm('确定要删除这条深度采集数据吗?', function() {
  237. $.ajax({
  238. url: '/deep/api/delete',
  239. type: 'POST',
  240. contentType: 'application/json',
  241. data: JSON.stringify({ ids: [id] }),
  242. success: function() {
  243. showToast('删除成功', 'success');
  244. loadData();
  245. },
  246. error: function() {
  247. showToast('删除失败', 'error');
  248. }
  249. });
  250. });
  251. }
  252. function batchDelete() {
  253. const ids = [];
  254. $('.row-checkbox:checked').each(function() {
  255. ids.push($(this).val());
  256. });
  257. if (ids.length === 0) {
  258. showToast('请先选择要删除的数据', 'info');
  259. return;
  260. }
  261. showConfirm(`确定要删除选中的 ${ids.length} 条数据吗?`, function() {
  262. $.ajax({
  263. url: '/deep/api/delete',
  264. type: 'POST',
  265. contentType: 'application/json',
  266. data: JSON.stringify({ ids: ids }),
  267. success: function() {
  268. showToast('批量删除成功', 'success');
  269. loadData();
  270. },
  271. error: function() {
  272. showToast('批量删除失败', 'error');
  273. }
  274. });
  275. });
  276. }
  277. // Pagination (Reuse from other pages or simple impl)
  278. function renderPagination(data) {
  279. const container = $('#pagination-controls');
  280. container.empty();
  281. if (data.pages <= 1) return;
  282. const prevBtn = $(`<button class="px-3 py-1 rounded border border-gray-600 text-gray-300 hover:bg-gray-700 disabled:opacity-50" ${data.current_page === 1 ? 'disabled' : ''}><i class="fas fa-chevron-left"></i></button>`);
  283. prevBtn.click(() => {
  284. if (currentPage > 1) {
  285. currentPage--;
  286. loadData();
  287. }
  288. });
  289. container.append(prevBtn);
  290. const nextBtn = $(`<button class="px-3 py-1 rounded border border-gray-600 text-gray-300 hover:bg-gray-700 disabled:opacity-50" ${data.current_page === data.pages ? 'disabled' : ''}><i class="fas fa-chevron-right"></i></button>`);
  291. nextBtn.click(() => {
  292. if (currentPage < data.pages) {
  293. currentPage++;
  294. loadData();
  295. }
  296. });
  297. container.append(nextBtn);
  298. }
  299. function toggleSelectAll() {
  300. const isChecked = $('#select-all').prop('checked');
  301. $('.row-checkbox').prop('checked', isChecked);
  302. }
  303. // Shared functions (showToast, showConfirm) - assumed to be global or we copy them
  304. // Copying simple versions here to be safe
  305. function showToast(message, type = 'info') {
  306. const toast = $('#toast');
  307. const icon = $('#toast-icon');
  308. const msg = $('#toast-message');
  309. msg.text(message);
  310. if (type === 'success') icon.attr('class', 'fas fa-check-circle text-green-400');
  311. else if (type === 'error') icon.attr('class', 'fas fa-times-circle text-red-400');
  312. else icon.attr('class', 'fas fa-info-circle text-blue-400');
  313. toast.removeClass('hidden').css('opacity', 0).animate({ opacity: 1 }, 300);
  314. setTimeout(() => {
  315. toast.animate({ opacity: 0 }, 300, function() {
  316. $(this).addClass('hidden');
  317. });
  318. }, 3000);
  319. }
  320. let confirmCallback = null;
  321. function showConfirm(message, callback) {
  322. $('#confirm-message').text(message);
  323. $('#confirm-modal').removeClass('hidden');
  324. confirmCallback = callback;
  325. }
  326. function closeConfirm() {
  327. $('#confirm-modal').addClass('hidden');
  328. confirmCallback = null;
  329. }
  330. $('#confirm-btn').click(function() {
  331. if (confirmCallback) confirmCallback();
  332. closeConfirm();
  333. });
  334. // --- Import Modal ---
  335. let currentImportId = null;
  336. function openImportModal(id) {
  337. currentImportId = id;
  338. // 获取当前行标题
  339. const row = $(`.row-checkbox[value="${id}"]`).closest('tr');
  340. const title = row.find('a[title]').attr('title') || `ID: ${id}`;
  341. $('#import-item-title').text(title);
  342. // 加载知识库列表
  343. const select = $('#import-kb-select');
  344. select.html('<option value="">加载中...</option>');
  345. $.get('/deep/api/knowledge-bases', function(response) {
  346. select.empty();
  347. const items = response.items || response.data?.items || [];
  348. if (items.length === 0) {
  349. select.html('<option value="">暂无知识库</option>');
  350. return;
  351. }
  352. items.forEach(kb => {
  353. select.append(`<option value="${kb.id}">${kb.name}</option>`);
  354. });
  355. }).fail(function() {
  356. select.html('<option value="">加载失败</option>');
  357. showToast('知识库列表加载失败', 'error');
  358. });
  359. $('#import-modal').removeClass('hidden');
  360. }
  361. function closeImportModal() {
  362. $('#import-modal').addClass('hidden');
  363. currentImportId = null;
  364. }
  365. $('#import-submit-btn').click(function() {
  366. const kbId = $('#import-kb-select').val();
  367. const kbName = $('#import-kb-select option:selected').text();
  368. if (!kbId) {
  369. showToast('请选择知识库', 'info');
  370. return;
  371. }
  372. $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> 提交中...');
  373. $.ajax({
  374. url: '/deep/api/import',
  375. type: 'POST',
  376. contentType: 'application/json',
  377. data: JSON.stringify({
  378. deep_id: currentImportId,
  379. kb_id: kbId,
  380. kb_name: kbName,
  381. }),
  382. success: function(response) {
  383. closeImportModal();
  384. if (response.code === '000000') {
  385. showToast('入库任务已提交,请在样本中心查看进度', 'success');
  386. } else {
  387. showToast('入库失败: ' + (response.message || '未知错误'), 'error');
  388. }
  389. },
  390. error: function() {
  391. showToast('入库请求失败', 'error');
  392. },
  393. complete: function() {
  394. $('#import-submit-btn').prop('disabled', false).html('<i class="fas fa-upload"></i> 提交入库');
  395. }
  396. });
  397. });
  398. </script>
  399. {% endblock %}