deep_management.html 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  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. <script>
  113. let currentPage = 1;
  114. let currentKeyword = '';
  115. const pageSize = 10;
  116. // Check URL params for initial query
  117. const urlParams = new URLSearchParams(window.location.search);
  118. const initialQuery = urlParams.get('query');
  119. $(document).ready(function() {
  120. if (initialQuery) {
  121. $('#search-keyword').val(initialQuery);
  122. currentKeyword = initialQuery;
  123. }
  124. loadData();
  125. $('#search-keyword').keypress(function(e) {
  126. if(e.which == 13) {
  127. searchData();
  128. }
  129. });
  130. $('#open-sidebar').click(function() {
  131. $('#sidebar').toggleClass('-translate-x-full');
  132. });
  133. });
  134. function searchData() {
  135. currentKeyword = $('#search-keyword').val().trim();
  136. currentPage = 1;
  137. loadData();
  138. }
  139. function loadData() {
  140. const tbody = $('#data-table-body');
  141. 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>');
  142. $.get('/deep/api/list', {
  143. page: currentPage,
  144. per_page: pageSize,
  145. query: currentKeyword
  146. }, function(response) {
  147. renderTable(response.items);
  148. renderPagination(response);
  149. $('#total-count').text(response.total);
  150. $('#select-all').prop('checked', false);
  151. }).fail(function() {
  152. tbody.html('<tr><td colspan="7" class="text-center py-10 text-red-500">加载失败,请重试</td></tr>');
  153. });
  154. }
  155. function renderTable(items) {
  156. const tbody = $('#data-table-body');
  157. tbody.empty();
  158. if (items.length === 0) {
  159. tbody.html('<tr><td colspan="7" class="text-center py-10 text-gray-500">暂无数据</td></tr>');
  160. return;
  161. }
  162. items.forEach(item => {
  163. const html = `
  164. <tr class="hover:bg-gray-800/50 transition-colors group">
  165. <td class="p-4 text-center">
  166. <input type="checkbox" class="row-checkbox rounded bg-gray-700 border-gray-600 text-blue-500 focus:ring-offset-gray-900" value="${item.id}">
  167. </td>
  168. <td class="p-4 text-gray-500 text-sm">#${item.id}</td>
  169. <td class="p-4">
  170. <div class="flex flex-col">
  171. <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}">
  172. ${item.title || item.url}
  173. </a>
  174. <div class="text-xs text-gray-500 line-clamp-1 font-mono">${item.url}</div>
  175. </div>
  176. </td>
  177. <td class="p-4">
  178. <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'}">
  179. ${item.status === 'completed' ? '已完成' : '失败'}
  180. </span>
  181. </td>
  182. <td class="p-4 text-gray-400 text-sm line-clamp-2" title="${item.summary || ''}">
  183. ${item.summary || '无摘要'}
  184. </td>
  185. <td class="p-4 text-sm text-gray-400 font-mono">${item.updated_at}</td>
  186. <td class="p-4">
  187. <div class="flex justify-center gap-2">
  188. <button onclick="viewDeepItem(${item.id})" class="text-blue-400 hover:text-blue-300 p-1" title="查看详情">
  189. <i class="fas fa-eye"></i>
  190. </button>
  191. <button onclick="deleteItem(${item.id})" class="text-red-400 hover:text-red-300 p-1" title="删除">
  192. <i class="fas fa-trash-alt"></i>
  193. </button>
  194. </div>
  195. </td>
  196. </tr>
  197. `;
  198. tbody.append(html);
  199. });
  200. }
  201. function viewDeepItem(id) {
  202. $.get('/deep/api/get/' + id, function(data) {
  203. $('#view-url').text(data.url);
  204. $('#view-summary').text(data.summary || '暂无摘要');
  205. $('#view-content').text(data.content || '暂无内容');
  206. $('#view-modal').removeClass('hidden');
  207. }).fail(function() {
  208. showToast('获取详情失败', 'error');
  209. });
  210. }
  211. function closeViewModal() {
  212. $('#view-modal').addClass('hidden');
  213. }
  214. function deleteItem(id) {
  215. showConfirm('确定要删除这条深度采集数据吗?', function() {
  216. $.ajax({
  217. url: '/deep/api/delete',
  218. type: 'POST',
  219. contentType: 'application/json',
  220. data: JSON.stringify({ ids: [id] }),
  221. success: function() {
  222. showToast('删除成功', 'success');
  223. loadData();
  224. },
  225. error: function() {
  226. showToast('删除失败', 'error');
  227. }
  228. });
  229. });
  230. }
  231. function batchDelete() {
  232. const ids = [];
  233. $('.row-checkbox:checked').each(function() {
  234. ids.push($(this).val());
  235. });
  236. if (ids.length === 0) {
  237. showToast('请先选择要删除的数据', 'info');
  238. return;
  239. }
  240. showConfirm(`确定要删除选中的 ${ids.length} 条数据吗?`, function() {
  241. $.ajax({
  242. url: '/deep/api/delete',
  243. type: 'POST',
  244. contentType: 'application/json',
  245. data: JSON.stringify({ ids: ids }),
  246. success: function() {
  247. showToast('批量删除成功', 'success');
  248. loadData();
  249. },
  250. error: function() {
  251. showToast('批量删除失败', 'error');
  252. }
  253. });
  254. });
  255. }
  256. // Pagination (Reuse from other pages or simple impl)
  257. function renderPagination(data) {
  258. const container = $('#pagination-controls');
  259. container.empty();
  260. if (data.pages <= 1) return;
  261. 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>`);
  262. prevBtn.click(() => {
  263. if (currentPage > 1) {
  264. currentPage--;
  265. loadData();
  266. }
  267. });
  268. container.append(prevBtn);
  269. 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>`);
  270. nextBtn.click(() => {
  271. if (currentPage < data.pages) {
  272. currentPage++;
  273. loadData();
  274. }
  275. });
  276. container.append(nextBtn);
  277. }
  278. function toggleSelectAll() {
  279. const isChecked = $('#select-all').prop('checked');
  280. $('.row-checkbox').prop('checked', isChecked);
  281. }
  282. // Shared functions (showToast, showConfirm) - assumed to be global or we copy them
  283. // Copying simple versions here to be safe
  284. function showToast(message, type = 'info') {
  285. const toast = $('#toast');
  286. const icon = $('#toast-icon');
  287. const msg = $('#toast-message');
  288. msg.text(message);
  289. if (type === 'success') icon.attr('class', 'fas fa-check-circle text-green-400');
  290. else if (type === 'error') icon.attr('class', 'fas fa-times-circle text-red-400');
  291. else icon.attr('class', 'fas fa-info-circle text-blue-400');
  292. toast.removeClass('hidden').css('opacity', 0).animate({ opacity: 1 }, 300);
  293. setTimeout(() => {
  294. toast.animate({ opacity: 0 }, 300, function() {
  295. $(this).addClass('hidden');
  296. });
  297. }, 3000);
  298. }
  299. let confirmCallback = null;
  300. function showConfirm(message, callback) {
  301. $('#confirm-message').text(message);
  302. $('#confirm-modal').removeClass('hidden');
  303. confirmCallback = callback;
  304. }
  305. function closeConfirm() {
  306. $('#confirm-modal').addClass('hidden');
  307. confirmCallback = null;
  308. }
  309. $('#confirm-btn').click(function() {
  310. if (confirmCallback) confirmCallback();
  311. closeConfirm();
  312. });
  313. </script>
  314. {% endblock %}