ソースを参照

feat: 新增手动入库功能,支持不依赖爬虫直接录入数据

kinglee 1 週間 前
コミット
feeaac624e
2 ファイル変更179 行追加0 行削除
  1. 46 0
      app/routes/collection_routes.py
  2. 133 0
      app/templates/collection_management.html

+ 46 - 0
app/routes/collection_routes.py

@@ -209,6 +209,52 @@ def save_selected():
         'task_id': task.id
         'task_id': task.id
     })
     })
 
 
+@bp.route('/api/manual-save', methods=['POST'])
+@login_required
+def manual_save():
+    data = request.json
+    items = data.get('items', [])
+
+    if not items:
+        return jsonify({'error': '没有要保存的数据'}), 400
+
+    added_count = 0
+    updated_count = 0
+
+    for item in items:
+        link = item.get('link', '').strip()
+        if not link:
+            continue
+
+        existing = SpiderResult.query.filter_by(link=link).first()
+
+        if existing:
+            existing.title = item.get('title')
+            existing.abstract = item.get('abstract')
+            existing.source = item.get('source')
+            existing.cover = item.get('cover')
+            existing.published_at = item.get('published_at')
+            updated_count += 1
+        else:
+            result = SpiderResult(
+                task_id=None,
+                title=item.get('title'),
+                abstract=item.get('abstract'),
+                source=item.get('source'),
+                cover=item.get('cover'),
+                link=link,
+                published_at=item.get('published_at')
+            )
+            db.session.add(result)
+            added_count += 1
+
+    db.session.commit()
+    return jsonify({
+        'message': '保存成功',
+        'added': added_count,
+        'updated': updated_count
+    })
+
 @bp.route('/api/results/<int:task_id>', methods=['GET'])
 @bp.route('/api/results/<int:task_id>', methods=['GET'])
 @login_required
 @login_required
 def get_results(task_id):
 def get_results(task_id):

+ 133 - 0
app/templates/collection_management.html

@@ -57,6 +57,9 @@
                     <div class="flex justify-between items-center mb-4">
                     <div class="flex justify-between items-center mb-4">
                         <label class="text-gray-400 text-sm font-bold">3. 采集结果橱窗</label>
                         <label class="text-gray-400 text-sm font-bold">3. 采集结果橱窗</label>
                         <div class="flex gap-2 items-center">
                         <div class="flex gap-2 items-center">
+                            <button onclick="openManualModal()" class="bg-purple-600 hover:bg-purple-700 text-white px-4 py-1 rounded text-sm transition-colors">
+                                <i class="fas fa-plus"></i> 手动入库
+                            </button>
                             <span id="result-count" class="text-gray-500 text-sm"></span>
                             <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">
                             <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> 全选
                                 <i class="fas fa-check-double"></i> 全选
@@ -380,5 +383,135 @@
             }
             }
         });
         });
     }
     }
+
+    function openManualModal() {
+        $('#manual-modal').removeClass('hidden');
+        $('#manual-link').val('');
+        $('#manual-title').val('');
+        $('#manual-abstract').val('');
+        $('#manual-source').val('');
+        $('#manual-cover').val('');
+        $('#manual-date').val('');
+    }
+
+    function closeManualModal() {
+        $('#manual-modal').addClass('hidden');
+    }
+
+    function addManualEntry() {
+        const link = $('#manual-link').val().trim();
+        if (!link) {
+            alert('链接地址为必填项');
+            return;
+        }
+
+        const item = {
+            title: $('#manual-title').val().trim(),
+            abstract: $('#manual-abstract').val().trim(),
+            source: $('#manual-source').val().trim(),
+            cover: $('#manual-cover').val().trim(),
+            link: link,
+            published_at: $('#manual-date').val().trim()
+        };
+
+        $.ajax({
+            url: '/collection/api/manual-save',
+            method: 'POST',
+            contentType: 'application/json',
+            data: JSON.stringify({ items: [item] }),
+            success: function(response) {
+                const msg = response.added > 0 ? `新增 ${response.added} 条` : `更新 ${response.updated} 条`;
+                alert('保存成功!' + msg);
+                closeManualModal();
+                // Add to showcase display
+                const idx = currentResults.length;
+                currentResults.push(item);
+                const container = $('#results-container');
+                // Remove placeholder if exists
+                if (container.find('.col-span-full').length > 0) {
+                    container.empty();
+                }
+                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-${idx}" 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(${idx}, 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.published_at || '未知时间'}
+                                </span>
+                            </div>
+                        </div>
+                    </div>
+                `;
+                container.append(html);
+                $('#btn-save').removeClass('hidden');
+                $('#result-count').text(`共 ${currentResults.length} 条数据`);
+            },
+            error: function(xhr) {
+                alert('保存失败: ' + (xhr.responseJSON?.error || '未知错误'));
+            }
+        });
+    }
 </script>
 </script>
+
+<!-- Manual Input Modal -->
+<div id="manual-modal" class="hidden fixed inset-0 bg-black/60 z-50 flex items-center justify-center">
+    <div class="bg-gray-800 rounded-lg shadow-xl p-6 w-full max-w-lg mx-4 border border-gray-700">
+        <div class="flex justify-between items-center mb-4">
+            <h2 class="text-xl font-bold text-white"><i class="fas fa-plus text-purple-500 mr-2"></i>手动入库</h2>
+            <button onclick="closeManualModal()" class="text-gray-400 hover:text-white">
+                <i class="fas fa-times text-xl"></i>
+            </button>
+        </div>
+        <div class="space-y-4">
+            <div>
+                <label class="block text-gray-400 text-sm mb-1 font-bold">链接地址 <span class="text-red-500">*</span></label>
+                <input type="text" id="manual-link" class="w-full bg-gray-700 text-white rounded p-3 focus:outline-none focus:ring-2 focus:ring-purple-500" placeholder="https://...">
+            </div>
+            <div>
+                <label class="block text-gray-400 text-sm mb-1 font-bold">标题</label>
+                <input type="text" id="manual-title" class="w-full bg-gray-700 text-white rounded p-3 focus:outline-none focus:ring-2 focus:ring-purple-500" placeholder="文章标题">
+            </div>
+            <div>
+                <label class="block text-gray-400 text-sm mb-1 font-bold">摘要</label>
+                <textarea id="manual-abstract" rows="3" class="w-full bg-gray-700 text-white rounded p-3 focus:outline-none focus:ring-2 focus:ring-purple-500" placeholder="内容摘要"></textarea>
+            </div>
+            <div class="flex gap-4">
+                <div class="flex-1">
+                    <label class="block text-gray-400 text-sm mb-1 font-bold">来源</label>
+                    <input type="text" id="manual-source" class="w-full bg-gray-700 text-white rounded p-3 focus:outline-none focus:ring-2 focus:ring-purple-500" placeholder="来源网站">
+                </div>
+                <div class="flex-1">
+                    <label class="block text-gray-400 text-sm mb-1 font-bold">发布日期</label>
+                    <input type="text" id="manual-date" class="w-full bg-gray-700 text-white rounded p-3 focus:outline-none focus:ring-2 focus:ring-purple-500" placeholder="2026-05-20">
+                </div>
+            </div>
+            <div>
+                <label class="block text-gray-400 text-sm mb-1 font-bold">封面图片 URL</label>
+                <input type="text" id="manual-cover" class="w-full bg-gray-700 text-white rounded p-3 focus:outline-none focus:ring-2 focus:ring-purple-500" placeholder="https://...">
+            </div>
+        </div>
+        <div class="flex justify-end gap-3 mt-6">
+            <button onclick="closeManualModal()" class="bg-gray-600 hover:bg-gray-500 text-white px-6 py-2 rounded transition-colors">取消</button>
+            <button onclick="addManualEntry()" class="bg-purple-600 hover:bg-purple-700 text-white px-6 py-2 rounded transition-colors">
+                <i class="fas fa-save mr-1"></i>保存入库
+            </button>
+        </div>
+    </div>
+</div>
 {% endblock %}
 {% endblock %}