collection_management.html 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. {% extends "base.html" %}
  2. {% block content %}
  3. <div class="flex h-screen overflow-hidden" id="collection-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">采集任务管理</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="h-full flex flex-col gap-6">
  24. <!-- 1. Keyword Input -->
  25. <div class="bg-gray-800 p-6 rounded-lg shadow-lg">
  26. <label class="block text-gray-400 text-sm mb-2 font-bold">1. 采集关键词</label>
  27. <div class="flex gap-4 items-end">
  28. <div class="flex-1">
  29. <input type="text" id="keyword" class="w-full bg-gray-700 text-white rounded p-3 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="请输入关键词...">
  30. </div>
  31. <div id="page-count-container" class="hidden w-32">
  32. <label class="block text-gray-400 text-sm mb-1 font-bold">采集页数</label>
  33. <input type="number" id="page-count" class="w-full bg-gray-700 text-white rounded p-3 focus:outline-none focus:ring-2 focus:ring-blue-500" value="1" min="1" max="100">
  34. </div>
  35. <button onclick="startCollection()" class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded font-bold transition-colors flex items-center gap-2 h-[50px]">
  36. <i class="fas fa-play"></i> 开始采集
  37. </button>
  38. </div>
  39. </div>
  40. <!-- 2. Spider Source Switch Panel -->
  41. <div class="bg-gray-800 p-6 rounded-lg shadow-lg">
  42. <label class="block text-gray-400 text-sm mb-4 font-bold">2. 选择爬虫源 (开关模式)</label>
  43. <div id="source-list" class="flex flex-wrap gap-4">
  44. <!-- Sources will be injected here -->
  45. <div class="animate-pulse bg-gray-700 h-10 w-32 rounded"></div>
  46. </div>
  47. </div>
  48. <!-- 3. Showcase Data List -->
  49. <div class="flex-1 bg-gray-800 p-6 rounded-lg shadow-lg flex flex-col min-h-0">
  50. <div class="flex justify-between items-center mb-4">
  51. <label class="text-gray-400 text-sm font-bold">3. 采集结果橱窗</label>
  52. <div class="flex gap-2 items-center">
  53. <span id="result-count" class="text-gray-500 text-sm"></span>
  54. <button onclick="toggleSelectAll()" id="btn-select-all" class="hidden bg-gray-600 hover:bg-gray-500 text-white px-3 py-1 rounded text-sm transition-colors">
  55. <i class="fas fa-check-double"></i> 全选
  56. </button>
  57. <button onclick="saveSelected()" id="btn-save" class="hidden bg-green-600 hover:bg-green-700 text-white px-4 py-1 rounded text-sm transition-colors">
  58. <i class="fas fa-save"></i> 保存选中数据
  59. </button>
  60. </div>
  61. </div>
  62. <div id="results-container" class="flex-1 overflow-y-auto grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 pr-2 custom-scrollbar" style="min-height: 300px;">
  63. <!-- Results will be injected here -->
  64. <div class="col-span-full text-center text-gray-500 py-20">
  65. <i class="fas fa-search text-4xl mb-4 opacity-50"></i>
  66. <p>请输入关键词并选择爬虫源开始采集</p>
  67. </div>
  68. </div>
  69. </div>
  70. </div>
  71. </main>
  72. </div>
  73. </div>
  74. <style>
  75. /* Custom Switch Style */
  76. .source-switch input:checked + div {
  77. background-color: #3B82F6;
  78. color: white;
  79. border-color: #3B82F6;
  80. }
  81. .source-switch input:checked + div .check-icon {
  82. display: block;
  83. }
  84. /* Card Selection Style */
  85. .result-card.selected {
  86. border-color: #3B82F6;
  87. background-color: #1F2937; /* Slightly lighter gray */
  88. position: relative;
  89. }
  90. .result-card.selected::after {
  91. content: '\f00c';
  92. font-family: 'Font Awesome 5 Free';
  93. font-weight: 900;
  94. position: absolute;
  95. top: 10px;
  96. right: 10px;
  97. background: #3B82F6;
  98. color: white;
  99. width: 24px;
  100. height: 24px;
  101. border-radius: 50%;
  102. display: flex;
  103. align-items: center;
  104. justify-content: center;
  105. font-size: 12px;
  106. }
  107. </style>
  108. <script>
  109. let currentResults = [];
  110. let selectedIndices = new Set();
  111. let sourcesMap = {};
  112. $(document).ready(function() {
  113. loadSources();
  114. // Sidebar Toggle Logic
  115. $('#open-sidebar').click(function() {
  116. $('.sidebar').toggleClass('-translate-x-full');
  117. });
  118. });
  119. function loadSources() {
  120. $.get('/collection/api/sources', function(data) {
  121. const container = $('#source-list');
  122. container.empty();
  123. sourcesMap = {};
  124. if (data.length === 0) {
  125. container.html('<p class="text-gray-500">暂无可用爬虫源</p>');
  126. return;
  127. }
  128. data.forEach((source, index) => {
  129. sourcesMap[source.id] = source;
  130. const html = `
  131. <label class="source-switch cursor-pointer select-none">
  132. <input type="radio" name="spider_source" value="${source.id}" class="hidden" ${index === 0 ? 'checked' : ''}>
  133. <div class="bg-gray-700 border border-gray-600 rounded-full px-6 py-2 text-gray-300 transition-all hover:bg-gray-600 flex items-center gap-2">
  134. <span>${source.name}</span>
  135. <i class="fas fa-check check-icon hidden text-xs"></i>
  136. </div>
  137. </label>
  138. `;
  139. container.append(html);
  140. });
  141. $('input[name="spider_source"]').change(updatePageInputVisibility);
  142. if (data.length > 0) updatePageInputVisibility();
  143. });
  144. }
  145. function updatePageInputVisibility() {
  146. const sourceId = $('input[name="spider_source"]:checked').val();
  147. const source = sourcesMap[sourceId];
  148. if (source && source.has_pagination) {
  149. $('#page-count-container').removeClass('hidden');
  150. } else {
  151. $('#page-count-container').addClass('hidden');
  152. }
  153. }
  154. function startCollection() {
  155. const keyword = $('#keyword').val().trim();
  156. const sourceId = $('input[name="spider_source"]:checked').val();
  157. const pageCount = $('#page-count').val();
  158. if (!keyword) {
  159. alert('请输入关键词');
  160. return;
  161. }
  162. if (!sourceId) {
  163. alert('请选择爬虫源');
  164. return;
  165. }
  166. // Reset UI
  167. const container = $('#results-container');
  168. container.html(`
  169. <div class="col-span-full text-center text-gray-500 py-20">
  170. <i class="fas fa-circle-notch fa-spin text-4xl mb-4 text-blue-500"></i>
  171. <p>正在采集数据,请稍候...</p>
  172. </div>
  173. `);
  174. $('#btn-save').addClass('hidden');
  175. $('#result-count').text('');
  176. currentResults = [];
  177. selectedIndices.clear();
  178. // Call API
  179. $.ajax({
  180. url: '/collection/api/preview',
  181. method: 'POST',
  182. contentType: 'application/json',
  183. data: JSON.stringify({
  184. keyword: keyword,
  185. source_id: sourceId,
  186. pages: pageCount
  187. }),
  188. success: function(response) {
  189. renderResults(response.results);
  190. },
  191. error: function(xhr) {
  192. container.html(`
  193. <div class="col-span-full text-center text-red-500 py-20">
  194. <i class="fas fa-exclamation-circle text-4xl mb-4"></i>
  195. <p>采集失败: ${xhr.responseJSON?.error || '未知错误'}</p>
  196. </div>
  197. `);
  198. }
  199. });
  200. }
  201. function renderResults(results) {
  202. const container = $('#results-container');
  203. container.empty();
  204. currentResults = results;
  205. selectedIndices.clear(); // Reset selection on new search
  206. $('#btn-select-all').text('全选').removeClass('bg-blue-600').addClass('bg-gray-600');
  207. if (results.length === 0) {
  208. container.html(`
  209. <div class="col-span-full text-center text-gray-500 py-20">
  210. <p>未找到相关数据</p>
  211. </div>
  212. `);
  213. $('#btn-select-all').addClass('hidden');
  214. $('#btn-save').addClass('hidden');
  215. return;
  216. }
  217. results.forEach((item, index) => {
  218. const coverHtml = item.cover ?
  219. `<img src="${item.cover}" class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105" onerror="this.src='https://via.placeholder.com/300x200?text=No+Image'">` :
  220. `<div class="w-full h-full bg-gray-700 flex items-center justify-center text-gray-500"><i class="fas fa-image text-3xl"></i></div>`;
  221. const html = `
  222. <div id="result-card-${index}" class="result-card bg-gray-700 rounded-lg border border-gray-600 cursor-pointer hover:shadow-xl transition-all flex flex-col group relative"
  223. onclick="toggleSelection(${index}, this)">
  224. <div class="h-40 w-full flex-none overflow-hidden rounded-t-lg relative">
  225. ${coverHtml}
  226. </div>
  227. <div class="p-4 flex flex-col flex-1">
  228. <h3 class="text-white font-bold mb-2 text-sm md:text-base break-words hover:text-blue-400"
  229. style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.5em; max-height: 3em;"
  230. title="${item.title || ''}"
  231. onclick="event.stopPropagation(); window.open('${item.link}', '_blank')">
  232. ${item.title || '<span class="text-gray-500 italic">(无标题)</span>'}
  233. </h3>
  234. <div class="mt-auto pt-2 border-t border-gray-600 flex justify-between items-center text-xs text-gray-400">
  235. <span class="truncate max-w-[120px]" title="${item.source || ''}">
  236. <i class="fas fa-globe mr-1 text-blue-400"></i>${item.source || '未知来源'}
  237. </span>
  238. <span class="whitespace-nowrap ml-2">
  239. <i class="far fa-clock mr-1 text-green-400"></i>${item.date || '未知时间'}
  240. </span>
  241. </div>
  242. </div>
  243. </div>
  244. `;
  245. container.append(html);
  246. });
  247. $('#result-count').text(`共找到 ${results.length} 条数据`);
  248. $('#btn-save').removeClass('hidden').html(`<i class="fas fa-save"></i> 保存选中数据`);
  249. $('#btn-select-all').removeClass('hidden');
  250. if (container[0].children.length > 0) {
  251. container[0].children[0].scrollIntoView({ behavior: 'smooth' });
  252. }
  253. }
  254. function toggleSelection(index, element) {
  255. if (selectedIndices.has(index)) {
  256. selectedIndices.delete(index);
  257. $(element).removeClass('selected');
  258. } else {
  259. selectedIndices.add(index);
  260. $(element).addClass('selected');
  261. }
  262. updateSaveButton();
  263. }
  264. function toggleSelectAll() {
  265. const btn = $('#btn-select-all');
  266. // Check if current state is "Select All" (i.e. not "Cancel")
  267. const isSelectAll = !btn.text().includes('取消');
  268. if (isSelectAll) {
  269. // Select All
  270. currentResults.forEach((_, index) => {
  271. selectedIndices.add(index);
  272. $(`#result-card-${index}`).addClass('selected');
  273. });
  274. btn.html('<i class="fas fa-times"></i> 取消全选').removeClass('bg-gray-600').addClass('bg-blue-600');
  275. } else {
  276. // Deselect All
  277. selectedIndices.clear();
  278. $('.result-card.selected').removeClass('selected');
  279. btn.html('<i class="fas fa-check-double"></i> 全选').removeClass('bg-blue-600').addClass('bg-gray-600');
  280. }
  281. updateSaveButton();
  282. }
  283. function updateSaveButton() {
  284. const count = selectedIndices.size;
  285. $('#btn-save').html(`<i class="fas fa-save"></i> 保存选中数据 (${count})`);
  286. }
  287. function saveSelected() {
  288. if (selectedIndices.size === 0) {
  289. alert('请至少选择一条数据');
  290. return;
  291. }
  292. const itemsToSave = Array.from(selectedIndices).map(i => currentResults[i]);
  293. const keyword = $('#keyword').val().trim();
  294. const sourceId = $('input[name="spider_source"]:checked').val();
  295. $.ajax({
  296. url: '/collection/api/save_selected',
  297. method: 'POST',
  298. contentType: 'application/json',
  299. data: JSON.stringify({
  300. keyword: keyword,
  301. source_id: sourceId,
  302. items: itemsToSave
  303. }),
  304. success: function(response) {
  305. alert(`保存成功!\n新增: ${response.added}\n更新: ${response.updated}`);
  306. // Remove saved items from view
  307. Array.from(selectedIndices).sort((a, b) => b - a).forEach(index => {
  308. $(`#result-card-${index}`).remove();
  309. // Also remove from currentResults to keep indices valid?
  310. // Actually removing from DOM invalidates indices if we rely on them for future clicks.
  311. // But we just removed them, so we can't click them.
  312. // However, if we don't remove from currentResults, the indices in currentResults match the initial render.
  313. // But if we remove from DOM, the user can't select them again.
  314. // The issue is if I select item 0 and 2, save them, they are gone.
  315. // Item 1 is still there. If I click Item 1, it calls toggleSelection(1).
  316. // This is fine because I am NOT modifying currentResults array, just hiding/removing DOM elements.
  317. });
  318. // Clear selection
  319. selectedIndices.clear();
  320. updateSaveButton();
  321. // Reset Select All button if needed
  322. $('#btn-select-all').html('<i class="fas fa-check-double"></i> 全选').removeClass('bg-blue-600').addClass('bg-gray-600');
  323. // Update count
  324. const remaining = $('.result-card').length;
  325. $('#result-count').text(`剩余 ${remaining} 条数据`);
  326. if (remaining === 0) {
  327. $('#results-container').html(`
  328. <div class="col-span-full text-center text-gray-500 py-20">
  329. <p>所有数据已保存</p>
  330. </div>
  331. `);
  332. $('#btn-select-all').addClass('hidden');
  333. $('#btn-save').addClass('hidden');
  334. }
  335. },
  336. error: function(xhr) {
  337. alert('保存失败: ' + (xhr.responseJSON?.error || '未知错误'));
  338. }
  339. });
  340. }
  341. </script>
  342. {% endblock %}