| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383 |
- {% extends "base.html" %}
- {% block content %}
- <div class="flex h-screen overflow-hidden" id="spider-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">
- <!-- Source List Section -->
- <div class="tech-panel p-6 rounded-xl mb-6">
- <div class="flex justify-between items-center mb-4">
- <h3 class="text-lg font-semibold text-cyan-200"><i class="fas fa-database mr-2"></i>爬虫源列表</h3>
- <div>
- <button onclick="openModal()" class="tech-button px-4 py-2 bg-blue-600 text-white text-sm rounded hover:bg-blue-500 mr-2"><i class="fas fa-plus mr-1"></i> 新增</button>
- <button id="refresh-sources-btn" class="text-sm text-blue-400 hover:text-white"><i class="fas fa-sync-alt mr-1"></i>刷新</button>
- </div>
- </div>
- <div class="overflow-x-auto">
- <table class="w-full text-left border-collapse">
- <thead>
- <tr class="text-gray-400 border-b border-blue-800">
- <th class="p-3">ID</th>
- <th class="p-3">名称</th>
- <th class="p-3">标识</th>
- <th class="p-3">类型</th>
- <th class="p-3">描述</th>
- <th class="p-3">状态</th>
- <th class="p-3">操作</th>
- </tr>
- </thead>
- <tbody id="source-table-body" class="text-gray-300">
- <!-- Sources will be inserted here -->
- </tbody>
- </table>
- </div>
- </div>
- </main>
-
- <!-- Edit Modal -->
- <div id="edit-modal" class="fixed inset-0 z-50 hidden bg-black bg-opacity-50 flex items-center justify-center">
- <div class="bg-gray-800 rounded-lg shadow-lg w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto">
- <div class="p-4 border-b border-gray-700 flex justify-between items-center">
- <h3 class="text-xl font-bold text-white" id="modal-title">新增爬虫源</h3>
- <button onclick="closeModal()" class="text-gray-400 hover:text-white"><i class="fas fa-times"></i></button>
- </div>
- <div class="p-6">
- <form id="source-form">
- <input type="hidden" id="source-id" name="id">
-
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
- <div>
- <label class="block text-gray-400 text-sm font-bold mb-2">名称</label>
- <input type="text" name="name" id="source-name" class="w-full p-2 bg-gray-700 rounded border border-gray-600 text-white" required>
- </div>
- <div>
- <label class="block text-gray-400 text-sm font-bold mb-2">代码标识</label>
- <input type="text" name="code_identifier" id="source-code" class="w-full p-2 bg-gray-700 rounded border border-gray-600 text-white" required>
- </div>
- </div>
- <div class="mb-4">
- <label class="block text-gray-400 text-sm font-bold mb-2">描述</label>
- <textarea name="description" id="source-desc" class="w-full p-2 bg-gray-700 rounded border border-gray-600 text-white h-20"></textarea>
- </div>
- <div class="mb-4">
- <label class="block text-gray-400 text-sm font-bold mb-2">类型</label>
- <select name="type" id="source-type" class="w-full p-2 bg-gray-700 rounded border border-gray-600 text-white" onchange="toggleTypeFields()">
- <option value="script">自定义脚本 (app/spider/xxx.py)</option>
- <option value="generic">通用配置 (Generic Engine)</option>
- </select>
- </div>
- <!-- Generic Fields -->
- <div id="generic-fields" class="hidden space-y-4 border-t border-gray-700 pt-4">
- <h4 class="text-cyan-400 font-bold mb-2">通用爬虫配置</h4>
-
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
- <div>
- <label class="block text-gray-400 text-sm mb-1">请求 URL</label>
- <input type="text" name="url" id="source-url" class="w-full p-2 bg-gray-700 rounded border border-gray-600 text-white" placeholder="https://example.com/search">
- </div>
- <div>
- <label class="block text-gray-400 text-sm mb-1">请求方法</label>
- <select name="method" id="source-method" class="w-full p-2 bg-gray-700 rounded border border-gray-600 text-white">
- <option value="GET">GET</option>
- <option value="POST">POST</option>
- </select>
- </div>
- </div>
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
- <div>
- <label class="block text-gray-400 text-sm mb-1">搜索参数名</label>
- <input type="text" name="search_param_key" id="source-param-key" class="w-full p-2 bg-gray-700 rounded border border-gray-600 text-white" placeholder="q (default)">
- </div>
- </div>
- <div class="mt-2 mb-4 border-t border-gray-600 pt-2">
- <h4 class="text-sm font-bold text-gray-300 mb-2">分页配置</h4>
- <div class="flex items-center mb-2">
- <input type="checkbox" id="source-has-pagination" class="mr-2 rounded bg-gray-700 border-gray-600 text-cyan-600 focus:ring-cyan-600">
- <label for="source-has-pagination" class="text-gray-400 text-sm select-none cursor-pointer">启用分页</label>
- </div>
- <div id="pagination-fields" class="grid grid-cols-1 md:grid-cols-3 gap-4 hidden">
- <div>
- <label class="block text-gray-400 text-sm mb-1">分页参数名</label>
- <input type="text" id="source-pagination-param" class="w-full p-2 bg-gray-700 rounded border border-gray-600 text-white" placeholder="e.g. pn">
- </div>
- <div>
- <label class="block text-gray-400 text-sm mb-1">步长</label>
- <input type="number" id="source-pagination-step" class="w-full p-2 bg-gray-700 rounded border border-gray-600 text-white" value="10">
- </div>
- <div>
- <label class="block text-gray-400 text-sm mb-1">起始值</label>
- <input type="number" id="source-pagination-start" class="w-full p-2 bg-gray-700 rounded border border-gray-600 text-white" value="0">
- </div>
- </div>
- </div>
- <div>
- <label class="block text-gray-400 text-sm mb-1">Headers (JSON 或 Key: Value 格式)</label>
- <textarea name="headers" id="source-headers" class="w-full p-2 bg-gray-700 rounded border border-gray-600 text-white font-mono text-xs h-20" placeholder='{"User-Agent": "..."} 或
- User-Agent: Mozilla/5.0
- Accept: text/html'></textarea>
- </div>
-
- <div>
- <label class="block text-gray-400 text-sm mb-1">Selectors (JSON)</label>
- <textarea name="selectors" id="source-selectors" class="w-full p-2 bg-gray-700 rounded border border-gray-600 text-white font-mono text-xs h-32" placeholder='{
- "list": "div.result",
- "title": "h3 > a",
- "link": {"selector": "h3 > a", "attr": "href"},
- "abstract": "div.abstract"
- }'></textarea>
- <p class="text-xs text-gray-500 mt-1">支持 CSS 选择器。字段: list, title, link, abstract, source, cover</p>
- </div>
- </div>
- <div class="flex justify-end mt-6">
- <button type="button" onclick="closeModal()" class="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-500 mr-2">取消</button>
- <button type="submit" class="px-4 py-2 bg-cyan-600 text-white rounded hover:bg-cyan-500">保存</button>
- </div>
- </form>
- </div>
- </div>
- </div>
- </div>
- </div>
- {% endblock %}
- {% block scripts %}
- <script>
- // Sidebar Toggle Logic
- const sidebar = document.getElementById('sidebar');
- const openSidebarBtn = document.getElementById('open-sidebar');
- const closeSidebarBtn = document.getElementById('close-sidebar');
- function toggleSidebar() {
- sidebar.classList.toggle('-translate-x-full');
- }
- if (openSidebarBtn) openSidebarBtn.addEventListener('click', toggleSidebar);
- if (closeSidebarBtn) closeSidebarBtn.addEventListener('click', toggleSidebar);
- // Source Management Logic
- $(document).ready(function() {
- loadSources();
- $('#refresh-sources-btn').click(loadSources);
-
- $('#source-form').submit(function(e) {
- e.preventDefault();
- saveSource();
- });
- $('#source-has-pagination').change(function() {
- if(this.checked) {
- $('#pagination-fields').removeClass('hidden');
- } else {
- $('#pagination-fields').addClass('hidden');
- }
- });
- });
- function loadSources() {
- $.get('/spider-source/api/list', function(sources) {
- const tbody = $('#source-table-body');
- tbody.empty();
-
- if (sources.length === 0) {
- tbody.append('<tr><td colspan="7" class="p-3 text-center text-gray-500">暂无爬虫源</td></tr>');
- return;
- }
- sources.forEach(source => {
- let statusBadge = source.status === 'active'
- ? '<span class="px-2 py-1 rounded text-xs bg-green-900 text-green-300">启用</span>'
- : '<span class="px-2 py-1 rounded text-xs bg-red-900 text-red-300">禁用</span>';
-
- let typeBadge = source.type === 'generic'
- ? '<span class="px-2 py-1 rounded text-xs bg-purple-900 text-purple-300">通用</span>'
- : '<span class="px-2 py-1 rounded text-xs bg-yellow-900 text-yellow-300">脚本</span>';
- let actionBtn = source.status === 'active'
- ? `<button onclick="toggleSource(${source.id})" class="text-red-400 hover:text-red-300 mr-2" title="禁用"><i class="fas fa-ban"></i></button>`
- : `<button onclick="toggleSource(${source.id})" class="text-green-400 hover:text-green-300 mr-2" title="启用"><i class="fas fa-check"></i></button>`;
-
- actionBtn += `<button onclick="editSource(${source.id})" class="text-blue-400 hover:text-blue-300 mr-2" title="编辑"><i class="fas fa-edit"></i></button>`;
- actionBtn += `<button onclick="deleteSource(${source.id})" class="text-gray-400 hover:text-red-500" title="删除"><i class="fas fa-trash"></i></button>`;
- const tr = `
- <tr class="border-b border-blue-900/30 hover:bg-blue-900/20 transition">
- <td class="p-3">#${source.id}</td>
- <td class="p-3 font-bold text-cyan-400">${source.name}</td>
- <td class="p-3 font-mono text-sm text-yellow-300">${source.code_identifier}</td>
- <td class="p-3">${typeBadge}</td>
- <td class="p-3 text-sm text-gray-400 truncate max-w-xs">${source.description || '-'}</td>
- <td class="p-3">${statusBadge}</td>
- <td class="p-3">${actionBtn}</td>
- </tr>
- `;
- tbody.append(tr);
- });
- });
- }
- function toggleSource(sourceId) {
- if(!confirm('确定要更改该爬虫源的状态吗?')) return;
- $.post(`/spider-source/api/toggle/${sourceId}`, function() { loadSources(); }).fail(function() { alert('操作失败'); });
- }
-
- function deleteSource(sourceId) {
- if(!confirm('确定要删除该爬虫源吗?')) return;
- $.post(`/spider-source/api/delete/${sourceId}`, function() { loadSources(); }).fail(function(xhr) { alert(xhr.responseJSON.error || '操作失败'); });
- }
- function openModal(source = null) {
- const modal = $('#edit-modal');
- const form = $('#source-form')[0];
- form.reset();
-
- if (source) {
- $('#modal-title').text('编辑爬虫源');
- $('#source-id').val(source.id);
- $('#source-name').val(source.name);
- $('#source-code').val(source.code_identifier);
- $('#source-desc').val(source.description);
- $('#source-type').val(source.type || 'script');
-
- // Generic fields
- $('#source-url').val(source.url);
- $('#source-method').val(source.method || 'GET');
- $('#source-param-key').val(source.search_param_key || 'q');
- $('#source-headers').val(source.headers);
- $('#source-selectors').val(source.selectors);
- // Pagination fields
- $('#source-has-pagination').prop('checked', source.has_pagination || false).trigger('change');
- $('#source-pagination-param').val(source.pagination_param || 'pn');
- $('#source-pagination-step').val(source.pagination_step !== undefined ? source.pagination_step : 10);
- $('#source-pagination-start').val(source.pagination_start !== undefined ? source.pagination_start : 0);
- } else {
- $('#modal-title').text('新增爬虫源');
- $('#source-id').val('');
- $('#source-type').val('script'); // Default
-
- // Reset pagination defaults
- $('#source-has-pagination').prop('checked', false).trigger('change');
- $('#source-pagination-param').val('pn');
- $('#source-pagination-step').val(10);
- $('#source-pagination-start').val(0);
- }
-
- toggleTypeFields();
- modal.removeClass('hidden');
- }
-
- function closeModal() {
- $('#edit-modal').addClass('hidden');
- }
-
- function toggleTypeFields() {
- const type = $('#source-type').val();
- if (type === 'generic') {
- $('#generic-fields').removeClass('hidden');
- } else {
- $('#generic-fields').addClass('hidden');
- }
- }
-
- function editSource(id) {
- $.get(`/spider-source/api/get/${id}`, function(source) {
- openModal(source);
- });
- }
-
- function saveSource() {
- let headersInput = $('#source-headers').val().trim();
- let selectorsInput = $('#source-selectors').val().trim();
-
- // Try to parse headers if it doesn't look like JSON
- if (headersInput && !headersInput.trim().startsWith('{')) {
- try {
- const lines = headersInput.split('\n');
- const headersObj = {};
- let currentKey = null;
-
- for (let i = 0; i < lines.length; i++) {
- let line = lines[i].trim();
- if (!line) continue;
-
- if (line.endsWith(':')) {
- currentKey = line.substring(0, line.length - 1).trim();
- } else if (currentKey) {
- headersObj[currentKey] = line;
- currentKey = null;
- } else {
- const firstColon = line.indexOf(':');
- if (firstColon > -1) {
- const key = line.substring(0, firstColon).trim();
- const value = line.substring(firstColon + 1).trim();
- headersObj[key] = value;
- }
- }
- }
- headersInput = JSON.stringify(headersObj, null, 4);
- } catch (e) {
- console.error("Header parsing error:", e);
- // Fallback to original input if parsing fails, let backend or JSON validation handle it
- }
- }
- const data = {
- id: $('#source-id').val(),
- name: $('#source-name').val(),
- code_identifier: $('#source-code').val(),
- description: $('#source-desc').val(),
- type: $('#source-type').val(),
- url: $('#source-url').val(),
- method: $('#source-method').val(),
- search_param_key: $('#source-param-key').val(),
- headers: headersInput,
- selectors: selectorsInput,
- has_pagination: $('#source-has-pagination').is(':checked'),
- pagination_param: $('#source-pagination-param').val(),
- pagination_step: $('#source-pagination-step').val(),
- pagination_start: $('#source-pagination-start').val()
- };
-
- $.ajax({
- url: '/spider-source/api/save',
- method: 'POST',
- contentType: 'application/json',
- data: JSON.stringify(data),
- success: function() {
- closeModal();
- loadSources();
- alert('保存成功');
- },
- error: function(xhr) {
- alert('保存失败: ' + (xhr.responseJSON.error || 'Unknown error'));
- }
- });
- }
- </script>
- {% endblock %}
|