| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- {% extends "base.html" %}
- {% block content %}
- <div class="flex h-screen overflow-hidden" id="collection-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">
- <!-- 1. Keyword Input -->
- <div class="bg-gray-800 p-6 rounded-lg shadow-lg">
- <label class="block text-gray-400 text-sm mb-2 font-bold">1. 采集关键词</label>
- <div class="flex gap-4 items-end">
- <div class="flex-1">
- <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="请输入关键词...">
- </div>
-
- <div id="page-count-container" class="hidden w-32">
- <label class="block text-gray-400 text-sm mb-1 font-bold">采集页数</label>
- <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">
- </div>
- <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]">
- <i class="fas fa-play"></i> 开始采集
- </button>
- </div>
- </div>
- <!-- 2. Spider Source Switch Panel -->
- <div class="bg-gray-800 p-6 rounded-lg shadow-lg">
- <label class="block text-gray-400 text-sm mb-4 font-bold">2. 选择爬虫源 (开关模式)</label>
- <div id="source-list" class="flex flex-wrap gap-4">
- <!-- Sources will be injected here -->
- <div class="animate-pulse bg-gray-700 h-10 w-32 rounded"></div>
- </div>
- </div>
- <!-- 3. Showcase Data List -->
- <div class="flex-1 bg-gray-800 p-6 rounded-lg shadow-lg flex flex-col min-h-0">
- <div class="flex justify-between items-center mb-4">
- <label class="text-gray-400 text-sm font-bold">3. 采集结果橱窗</label>
- <div class="flex gap-2 items-center">
- <span id="result-count" class="text-gray-500 text-sm"></span>
- <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">
- <i class="fas fa-check-double"></i> 全选
- </button>
- <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">
- <i class="fas fa-save"></i> 保存选中数据
- </button>
- </div>
- </div>
-
- <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;">
- <!-- Results will be injected here -->
- <div class="col-span-full text-center text-gray-500 py-20">
- <i class="fas fa-search text-4xl mb-4 opacity-50"></i>
- <p>请输入关键词并选择爬虫源开始采集</p>
- </div>
- </div>
- </div>
- </div>
- </main>
- </div>
- </div>
- <style>
- /* Custom Switch Style */
- .source-switch input:checked + div {
- background-color: #3B82F6;
- color: white;
- border-color: #3B82F6;
- }
- .source-switch input:checked + div .check-icon {
- display: block;
- }
-
- /* Card Selection Style */
- .result-card.selected {
- border-color: #3B82F6;
- background-color: #1F2937; /* Slightly lighter gray */
- position: relative;
- }
- .result-card.selected::after {
- content: '\f00c';
- font-family: 'Font Awesome 5 Free';
- font-weight: 900;
- position: absolute;
- top: 10px;
- right: 10px;
- background: #3B82F6;
- color: white;
- width: 24px;
- height: 24px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 12px;
- }
- </style>
- <script>
- let currentResults = [];
- let selectedIndices = new Set();
- let sourcesMap = {};
- $(document).ready(function() {
- loadSources();
-
- // Sidebar Toggle Logic
- $('#open-sidebar').click(function() {
- $('.sidebar').toggleClass('-translate-x-full');
- });
- });
- function loadSources() {
- $.get('/collection/api/sources', function(data) {
- const container = $('#source-list');
- container.empty();
- sourcesMap = {};
-
- if (data.length === 0) {
- container.html('<p class="text-gray-500">暂无可用爬虫源</p>');
- return;
- }
- data.forEach((source, index) => {
- sourcesMap[source.id] = source;
- const html = `
- <label class="source-switch cursor-pointer select-none">
- <input type="radio" name="spider_source" value="${source.id}" class="hidden" ${index === 0 ? 'checked' : ''}>
- <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">
- <span>${source.name}</span>
- <i class="fas fa-check check-icon hidden text-xs"></i>
- </div>
- </label>
- `;
- container.append(html);
- });
-
- $('input[name="spider_source"]').change(updatePageInputVisibility);
- if (data.length > 0) updatePageInputVisibility();
- });
- }
- function updatePageInputVisibility() {
- const sourceId = $('input[name="spider_source"]:checked').val();
- const source = sourcesMap[sourceId];
-
- if (source && source.has_pagination) {
- $('#page-count-container').removeClass('hidden');
- } else {
- $('#page-count-container').addClass('hidden');
- }
- }
- function startCollection() {
- const keyword = $('#keyword').val().trim();
- const sourceId = $('input[name="spider_source"]:checked').val();
- const pageCount = $('#page-count').val();
- if (!keyword) {
- alert('请输入关键词');
- return;
- }
- if (!sourceId) {
- alert('请选择爬虫源');
- return;
- }
- // Reset UI
- const container = $('#results-container');
- container.html(`
- <div class="col-span-full text-center text-gray-500 py-20">
- <i class="fas fa-circle-notch fa-spin text-4xl mb-4 text-blue-500"></i>
- <p>正在采集数据,请稍候...</p>
- </div>
- `);
- $('#btn-save').addClass('hidden');
- $('#result-count').text('');
- currentResults = [];
- selectedIndices.clear();
- // Call API
- $.ajax({
- url: '/collection/api/preview',
- method: 'POST',
- contentType: 'application/json',
- data: JSON.stringify({
- keyword: keyword,
- source_id: sourceId,
- pages: pageCount
- }),
- success: function(response) {
- renderResults(response.results);
- },
- error: function(xhr) {
- container.html(`
- <div class="col-span-full text-center text-red-500 py-20">
- <i class="fas fa-exclamation-circle text-4xl mb-4"></i>
- <p>采集失败: ${xhr.responseJSON?.error || '未知错误'}</p>
- </div>
- `);
- }
- });
- }
- function renderResults(results) {
- const container = $('#results-container');
- container.empty();
- currentResults = results;
- selectedIndices.clear(); // Reset selection on new search
- $('#btn-select-all').text('全选').removeClass('bg-blue-600').addClass('bg-gray-600');
-
- if (results.length === 0) {
- container.html(`
- <div class="col-span-full text-center text-gray-500 py-20">
- <p>未找到相关数据</p>
- </div>
- `);
- $('#btn-select-all').addClass('hidden');
- $('#btn-save').addClass('hidden');
- return;
- }
- results.forEach((item, index) => {
- const coverHtml = item.cover ?
- `<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'">` :
- `<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>`;
-
- const html = `
- <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"
- onclick="toggleSelection(${index}, this)">
- <div class="h-40 w-full flex-none overflow-hidden rounded-t-lg relative">
- ${coverHtml}
- </div>
- <div class="p-4 flex flex-col flex-1">
- <h3 class="text-white font-bold mb-2 text-sm md:text-base break-words hover:text-blue-400"
- style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.5em; max-height: 3em;"
- title="${item.title || ''}"
- onclick="event.stopPropagation(); window.open('${item.link}', '_blank')">
- ${item.title || '<span class="text-gray-500 italic">(无标题)</span>'}
- </h3>
- <div class="mt-auto pt-2 border-t border-gray-600 flex justify-between items-center text-xs text-gray-400">
- <span class="truncate max-w-[120px]" title="${item.source || ''}">
- <i class="fas fa-globe mr-1 text-blue-400"></i>${item.source || '未知来源'}
- </span>
- <span class="whitespace-nowrap ml-2">
- <i class="far fa-clock mr-1 text-green-400"></i>${item.date || '未知时间'}
- </span>
- </div>
- </div>
- </div>
- `;
- container.append(html);
- });
- $('#result-count').text(`共找到 ${results.length} 条数据`);
- $('#btn-save').removeClass('hidden').html(`<i class="fas fa-save"></i> 保存选中数据`);
- $('#btn-select-all').removeClass('hidden');
-
- if (container[0].children.length > 0) {
- container[0].children[0].scrollIntoView({ behavior: 'smooth' });
- }
- }
- function toggleSelection(index, element) {
- if (selectedIndices.has(index)) {
- selectedIndices.delete(index);
- $(element).removeClass('selected');
- } else {
- selectedIndices.add(index);
- $(element).addClass('selected');
- }
-
- updateSaveButton();
- }
- function toggleSelectAll() {
- const btn = $('#btn-select-all');
- // Check if current state is "Select All" (i.e. not "Cancel")
- const isSelectAll = !btn.text().includes('取消');
-
- if (isSelectAll) {
- // Select All
- currentResults.forEach((_, index) => {
- selectedIndices.add(index);
- $(`#result-card-${index}`).addClass('selected');
- });
- btn.html('<i class="fas fa-times"></i> 取消全选').removeClass('bg-gray-600').addClass('bg-blue-600');
- } else {
- // Deselect All
- selectedIndices.clear();
- $('.result-card.selected').removeClass('selected');
- btn.html('<i class="fas fa-check-double"></i> 全选').removeClass('bg-blue-600').addClass('bg-gray-600');
- }
- updateSaveButton();
- }
- function updateSaveButton() {
- const count = selectedIndices.size;
- $('#btn-save').html(`<i class="fas fa-save"></i> 保存选中数据 (${count})`);
- }
- function saveSelected() {
- if (selectedIndices.size === 0) {
- alert('请至少选择一条数据');
- return;
- }
- const itemsToSave = Array.from(selectedIndices).map(i => currentResults[i]);
- const keyword = $('#keyword').val().trim();
- const sourceId = $('input[name="spider_source"]:checked').val();
- $.ajax({
- url: '/collection/api/save_selected',
- method: 'POST',
- contentType: 'application/json',
- data: JSON.stringify({
- keyword: keyword,
- source_id: sourceId,
- items: itemsToSave
- }),
- success: function(response) {
- alert(`保存成功!\n新增: ${response.added}\n更新: ${response.updated}`);
-
- // Remove saved items from view
- Array.from(selectedIndices).sort((a, b) => b - a).forEach(index => {
- $(`#result-card-${index}`).remove();
- // Also remove from currentResults to keep indices valid?
- // Actually removing from DOM invalidates indices if we rely on them for future clicks.
- // But we just removed them, so we can't click them.
- // However, if we don't remove from currentResults, the indices in currentResults match the initial render.
- // But if we remove from DOM, the user can't select them again.
- // The issue is if I select item 0 and 2, save them, they are gone.
- // Item 1 is still there. If I click Item 1, it calls toggleSelection(1).
- // This is fine because I am NOT modifying currentResults array, just hiding/removing DOM elements.
- });
- // Clear selection
- selectedIndices.clear();
- updateSaveButton();
-
- // Reset Select All button if needed
- $('#btn-select-all').html('<i class="fas fa-check-double"></i> 全选').removeClass('bg-blue-600').addClass('bg-gray-600');
-
- // Update count
- const remaining = $('.result-card').length;
- $('#result-count').text(`剩余 ${remaining} 条数据`);
-
- if (remaining === 0) {
- $('#results-container').html(`
- <div class="col-span-full text-center text-gray-500 py-20">
- <p>所有数据已保存</p>
- </div>
- `);
- $('#btn-select-all').addClass('hidden');
- $('#btn-save').addClass('hidden');
- }
- },
- error: function(xhr) {
- alert('保存失败: ' + (xhr.responseJSON?.error || '未知错误'));
- }
- });
- }
- </script>
- {% endblock %}
|