| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544 |
- {% extends "base.html" %}
- {% block content %}
- <div class="flex h-screen overflow-hidden" id="data-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="tech-panel p-6 rounded-xl mb-6 min-h-[calc(100vh-140px)] flex flex-col">
-
- <!-- 1. Search Area -->
- <div class="flex flex-col md:flex-row gap-4 mb-6">
- <div class="flex-1 flex gap-2">
- <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="请输入标题、来源或摘要进行模糊查询...">
- <button onclick="searchData()" class="bg-blue-600 hover:bg-blue-700 text-white px-6 rounded font-medium transition-colors">
- <i class="fas fa-search"></i> 搜索
- </button>
- </div>
- </div>
- <!-- 2. Toolbar -->
- <div class="flex gap-3 mb-4 p-3 bg-gray-800/50 rounded-lg border border-gray-700">
- <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">
- <i class="fas fa-trash-alt"></i> 批量删除
- </button>
- <button onclick="batchDeepCollect()" class="bg-purple-600/80 hover:bg-purple-600 text-white px-4 py-2 rounded text-sm transition-colors flex items-center gap-2">
- <i class="fas fa-robot"></i> 批量AI深度采集
- </button>
- </div>
- <!-- 3. Data Table -->
- <div class="flex-1 overflow-x-auto">
- <table class="w-full text-left border-collapse">
- <thead>
- <tr class="text-gray-400 border-b border-gray-700 bg-gray-800/30">
- <th class="p-4 w-12 text-center">
- <input type="checkbox" id="select-all" onclick="toggleSelectAll()" class="rounded bg-gray-700 border-gray-600 text-blue-500 focus:ring-offset-gray-900">
- </th>
- <th class="p-4 w-20">ID</th>
- <th class="p-4 w-24">封面</th>
- <th class="p-4">标题</th>
- <th class="p-4 w-40">来源</th>
- <th class="p-4 w-48">采集时间</th>
- <th class="p-4 w-48 text-center">操作</th>
- </tr>
- </thead>
- <tbody id="data-table-body" class="text-gray-300 divide-y divide-gray-700/50">
- <!-- Rows will be injected here -->
- </tbody>
- </table>
- </div>
- <!-- 4. Pagination -->
- <div class="mt-6 flex justify-between items-center border-t border-gray-700 pt-4">
- <div class="text-sm text-gray-500">
- 共 <span id="total-count" class="text-white font-bold">0</span> 条数据
- </div>
- <div class="flex gap-2" id="pagination-controls">
- <!-- Pagination buttons -->
- </div>
- </div>
- </div>
- </main>
- </div>
- </div>
- <!-- Toast Notification -->
- <div id="toast" class="fixed top-5 left-1/2 transform -translate-x-1/2 z-50 hidden transition-all duration-300">
- <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">
- <i class="fas fa-info-circle text-blue-400" id="toast-icon"></i>
- <span id="toast-message">Notification</span>
- </div>
- </div>
- <!-- Confirm Modal -->
- <div id="confirm-modal" class="fixed inset-0 z-50 hidden">
- <div class="absolute inset-0 bg-black/60 backdrop-blur-sm" onclick="closeConfirm()"></div>
- <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">
- <h3 class="text-xl font-bold text-white mb-2">确认操作</h3>
- <p class="text-gray-400 mb-6" id="confirm-message">确定要执行此操作吗?</p>
- <div class="flex justify-end gap-3">
- <button onclick="closeConfirm()" class="px-4 py-2 rounded text-gray-400 hover:text-white hover:bg-gray-700 transition-colors">取消</button>
- <button id="confirm-btn" class="px-4 py-2 rounded bg-red-600 text-white hover:bg-red-700 transition-colors shadow-lg">确定</button>
- </div>
- </div>
- </div>
- <!-- Deep Collect Report Modal -->
- <div id="deep-collect-modal" class="fixed inset-0 z-50 hidden">
- <div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
- <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-[500px] p-6 animate-fade-in-up">
- <h3 class="text-xl font-bold text-white mb-4"><i class="fas fa-robot text-purple-400 mr-2"></i>深度采集执行报告</h3>
-
- <div id="deep-progress-area" class="mb-6">
- <div class="flex justify-between text-sm text-gray-400 mb-2">
- <span id="deep-status-text">正在初始化...</span>
- <span id="deep-percent">0%</span>
- </div>
- <div class="w-full bg-gray-700 rounded-full h-2.5 overflow-hidden">
- <div id="deep-progress-bar" class="bg-purple-500 h-2.5 rounded-full transition-all duration-300" style="width: 0%"></div>
- </div>
- </div>
- <div id="deep-result-area" class="hidden mb-6 bg-gray-900/50 rounded p-4 border border-gray-700">
- <div class="flex items-center gap-2 mb-2">
- <i id="deep-result-icon" class="fas fa-check-circle text-green-500 text-lg"></i>
- <span id="deep-result-title" class="text-white font-bold">采集成功</span>
- </div>
- <p id="deep-result-msg" class="text-gray-400 text-sm mb-2">已成功采集并保存深度数据。</p>
- <div class="text-xs text-gray-500 bg-gray-800 p-2 rounded max-h-32 overflow-y-auto font-mono" id="deep-summary-preview">
- <!-- Summary -->
- </div>
- </div>
- <div class="flex justify-end gap-3">
- <button onclick="closeDeepModal()" class="px-4 py-2 rounded text-gray-400 hover:text-white hover:bg-gray-700 transition-colors" id="deep-close-btn">关闭</button>
- <a href="#" id="deep-view-btn" class="hidden px-4 py-2 rounded bg-blue-600 text-white hover:bg-blue-700 transition-colors shadow-lg">查看详情</a>
- </div>
- </div>
- </div>
- <script>
- let currentPage = 1;
- let currentKeyword = '';
- const pageSize = 10;
- $(document).ready(function() {
- loadData();
- // Bind Enter key for search
- $('#search-keyword').keypress(function(e) {
- if(e.which == 13) {
- searchData();
- }
- });
- // Sidebar Toggle
- $('#open-sidebar').click(function() {
- $('#sidebar').toggleClass('-translate-x-full');
- });
- });
- function searchData() {
- currentKeyword = $('#search-keyword').val().trim();
- currentPage = 1;
- loadData();
- }
- function loadData() {
- const tbody = $('#data-table-body');
- 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>');
- $.get('/data/api/list', {
- page: currentPage,
- per_page: pageSize,
- keyword: currentKeyword
- }, function(response) {
- renderTable(response.items);
- renderPagination(response);
- $('#total-count').text(response.total);
- // Reset Select All
- $('#select-all').prop('checked', false);
- }).fail(function() {
- tbody.html('<tr><td colspan="7" class="text-center py-10 text-red-500">加载失败,请重试</td></tr>');
- });
- }
- function renderTable(items) {
- const tbody = $('#data-table-body');
- tbody.empty();
- if (items.length === 0) {
- tbody.html('<tr><td colspan="7" class="text-center py-10 text-gray-500">暂无数据</td></tr>');
- return;
- }
- items.forEach(item => {
- const html = `
- <tr class="hover:bg-gray-800/50 transition-colors group">
- <td class="p-4 text-center">
- <input type="checkbox" class="row-checkbox rounded bg-gray-700 border-gray-600 text-blue-500 focus:ring-offset-gray-900" value="${item.id}">
- </td>
- <td class="p-4 text-gray-500 text-sm">#${item.id}</td>
- <td class="p-4">
- <div class="w-12 h-8 bg-gray-700 rounded overflow-hidden flex items-center justify-center">
- ${item.cover ? `<img src="${item.cover}" class="w-full h-full object-cover" onerror="this.style.display='none';this.nextElementSibling.style.display='inline'">` : ''}
- <i class="fas fa-image text-gray-500 text-xs ${item.cover ? 'hidden' : ''}"></i>
- </div>
- </td>
- <td class="p-4">
- <div class="flex items-center">
- <a href="${item.link}" target="_blank" class="text-white font-medium hover:text-blue-400 line-clamp-1 mr-2" title="${item.title}">
- ${item.title || '无标题'}
- </a>
- ${item.has_deep_collection ?
- `<a href="/deep/dashboard?query=${encodeURIComponent(item.link)}" class="inline-flex items-center gap-1 text-[10px] bg-purple-900/50 text-purple-300 px-1.5 py-0.5 rounded border border-purple-700 hover:bg-purple-800 transition-colors whitespace-nowrap" title="已深度采集">
- <i class="fas fa-check"></i> Deep
- </a>`
- : ''}
- </div>
- </td>
- <td class="p-4 text-sm text-gray-400">${item.source || '-'}</td>
- <td class="p-4 text-sm text-gray-400 font-mono">${item.published_at || '-'}</td>
- <td class="p-4 text-sm text-gray-400 font-mono">${item.created_at}</td>
- <td class="p-4">
- <div class="flex justify-center gap-2">
- <button onclick="deepCollectItem(${item.id})" class="text-purple-400 hover:text-purple-300 p-1" title="AI深度采集">
- <i class="fas fa-robot"></i>
- </button>
- <button onclick="deleteItem(${item.id})" class="text-red-400 hover:text-red-300 p-1" title="删除">
- <i class="fas fa-trash-alt"></i>
- </button>
- </div>
- </td>
- </tr>
- `;
- tbody.append(html);
- });
- }
-
- let deepPollInterval = null;
- function deepCollectItem(id) {
- $('#deep-collect-modal').removeClass('hidden');
- $('#deep-progress-area').show();
- $('#deep-result-area').addClass('hidden');
- $('#deep-view-btn').addClass('hidden');
- $('#deep-close-btn').prop('disabled', true).text('执行中...');
-
- updateProgress(0, '正在初始化任务...');
-
- $.ajax({
- url: '/deep/api/collect',
- type: 'POST',
- contentType: 'application/json',
- data: JSON.stringify({ id: id }),
- success: function(res) {
- const taskId = res.task_id;
- startDeepPolling(taskId);
- },
- error: function(xhr) {
- updateProgress(100, '启动失败');
- showDeepResult(false, xhr.responseJSON);
- }
- });
- }
- function startDeepPolling(taskId) {
- if (deepPollInterval) clearInterval(deepPollInterval);
-
- deepPollInterval = setInterval(() => {
- $.get(`/deep/api/status/${taskId}`, function(res) {
- updateProgress(res.progress, res.progress_msg);
-
- if (res.status === 'completed') {
- clearInterval(deepPollInterval);
- setTimeout(() => {
- showDeepResult(true, res);
- }, 500);
- } else if (res.status === 'failed') {
- clearInterval(deepPollInterval);
- showDeepResult(false, { error: res.error });
- }
- }).fail(function() {
- clearInterval(deepPollInterval);
- showDeepResult(false, { error: '获取进度失败' });
- });
- }, 1000);
- }
-
- function updateProgress(percent, text) {
- $('#deep-progress-bar').css('width', percent + '%');
- $('#deep-percent').text(percent + '%');
- $('#deep-status-text').text(text);
- }
-
- function showDeepResult(success, data) {
- $('#deep-progress-area').hide();
- $('#deep-result-area').removeClass('hidden');
- $('#deep-close-btn').prop('disabled', false).text('关闭');
-
- if (success) {
- $('#deep-result-icon').attr('class', 'fas fa-check-circle text-green-500 text-lg');
- $('#deep-result-title').text('采集成功');
- $('#deep-result-msg').text('深度采集数据已成功保存。');
- $('#deep-summary-preview').text(data.summary || '无摘要生成');
- $('#deep-view-btn').removeClass('hidden').attr('href', '/deep/dashboard?query=' + encodeURIComponent(data.url));
- loadData(); // Refresh list to show flag
- } else {
- $('#deep-result-icon').attr('class', 'fas fa-times-circle text-red-500 text-lg');
- $('#deep-result-title').text('采集失败');
- $('#deep-result-msg').text(data?.error || '未知错误');
- $('#deep-summary-preview').text('');
- }
- }
-
- function closeDeepModal() {
- if (deepPollInterval) clearInterval(deepPollInterval);
- $('#deep-collect-modal').addClass('hidden');
- }
- function renderPagination(data) {
- const container = $('#pagination-controls');
- container.empty();
- if (data.pages <= 1) return;
- // Previous
- container.append(`
- <button onclick="changePage(${data.current_page - 1})"
- class="px-3 py-1 rounded bg-gray-700 text-gray-300 hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
- ${data.current_page === 1 ? 'disabled' : ''}>
- <i class="fas fa-chevron-left"></i>
- </button>
- `);
- // Page Numbers (Simple logic for now: show all or simplified range)
- // Let's show current - 2 to current + 2
- let start = Math.max(1, data.current_page - 2);
- let end = Math.min(data.pages, data.current_page + 2);
- if (start > 1) {
- container.append(`<span class="px-2 text-gray-500">...</span>`);
- }
- for (let i = start; i <= end; i++) {
- container.append(`
- <button onclick="changePage(${i})"
- class="px-3 py-1 rounded ${i === data.current_page ? 'bg-blue-600 text-white' : 'bg-gray-700 text-gray-300 hover:bg-gray-600'}">
- ${i}
- </button>
- `);
- }
- if (end < data.pages) {
- container.append(`<span class="px-2 text-gray-500">...</span>`);
- }
- // Next
- container.append(`
- <button onclick="changePage(${data.current_page + 1})"
- class="px-3 py-1 rounded bg-gray-700 text-gray-300 hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
- ${data.current_page === data.pages ? 'disabled' : ''}>
- <i class="fas fa-chevron-right"></i>
- </button>
- `);
- }
- function changePage(page) {
- if (page < 1) return;
- currentPage = page;
- loadData();
- }
- function toggleSelectAll() {
- const checked = $('#select-all').prop('checked');
- $('.row-checkbox').prop('checked', checked);
- }
- function getSelectedIds() {
- const ids = [];
- $('.row-checkbox:checked').each(function() {
- ids.push($(this).val());
- });
- return ids;
- }
- // --- UI Helpers ---
- function showToast(msg, type = 'info') {
- const toast = $('#toast');
- const icon = $('#toast-icon');
- const text = $('#toast-message');
- const container = toast.find('div');
-
- text.text(msg);
- toast.removeClass('hidden').addClass('flex');
-
- // Reset classes
- container.removeClass('border-blue-500 border-red-500 border-green-500');
- icon.removeClass('text-blue-400 text-red-400 text-green-400 fa-info-circle fa-check-circle fa-exclamation-circle');
-
- if (type === 'success') {
- container.addClass('border-green-500');
- icon.addClass('text-green-400 fa-check-circle');
- } else if (type === 'error') {
- container.addClass('border-red-500');
- icon.addClass('text-red-400 fa-exclamation-circle');
- } else {
- container.addClass('border-blue-500');
- icon.addClass('text-blue-400 fa-info-circle');
- }
-
- setTimeout(() => {
- toast.addClass('hidden').removeClass('flex');
- }, 3000);
- }
- let onConfirmAction = null;
-
- function showConfirm(msg, action) {
- $('#confirm-message').text(msg);
- $('#confirm-modal').removeClass('hidden');
- onConfirmAction = action;
- }
- function closeConfirm() {
- $('#confirm-modal').addClass('hidden');
- onConfirmAction = null;
- }
-
- // Initialize listeners
- $(document).ready(function() {
- $('#confirm-btn').click(function() {
- if (onConfirmAction) onConfirmAction();
- closeConfirm();
- });
- });
- // --- Actions ---
- function deleteItem(id) {
- showConfirm('确定要删除这条数据吗?', () => performDelete([id]));
- }
- function batchDelete() {
- const ids = getSelectedIds();
- if (ids.length === 0) {
- showToast('请先选择要删除的数据', 'info');
- return;
- }
- showConfirm(`确定要删除选中的 ${ids.length} 条数据吗?`, () => performDelete(ids));
- }
- function performDelete(ids) {
- $.ajax({
- url: '/data/api/delete',
- method: 'POST',
- contentType: 'application/json',
- data: JSON.stringify({ ids: ids }),
- success: function(response) {
- showToast('删除成功', 'success');
- loadData(); // Reload current page
- },
- error: function(xhr) {
- showToast('删除失败: ' + (xhr.responseJSON?.error || '未知错误'), 'error');
- }
- });
- }
- async function batchDeepCollect() {
- const ids = getSelectedIds();
- if (ids.length === 0) {
- showToast('请先选择要进行AI深度采集的数据', 'info');
- return;
- }
-
- $('#deep-collect-modal').removeClass('hidden');
- $('#deep-progress-area').show();
- $('#deep-result-area').addClass('hidden');
- $('#deep-view-btn').addClass('hidden');
- $('#deep-close-btn').prop('disabled', true).text('批量执行中...');
-
- let successCount = 0;
- let failCount = 0;
-
- for (let i = 0; i < ids.length; i++) {
- const id = ids[i];
- const percent = Math.round(((i) / ids.length) * 100);
- updateProgress(percent, `正在处理第 ${i+1}/${ids.length} 条数据...`);
-
- try {
- // 1. Start Task
- const res = await new Promise((resolve, reject) => {
- $.ajax({
- url: '/deep/api/collect',
- type: 'POST',
- contentType: 'application/json',
- data: JSON.stringify({ id: id }),
- success: resolve,
- error: reject
- });
- });
-
- const taskId = res.task_id;
-
- // 2. Poll until complete
- await new Promise((resolve) => {
- const poll = setInterval(() => {
- $.get(`/deep/api/status/${taskId}`, function(statusRes) {
- if (statusRes.status === 'completed') {
- clearInterval(poll);
- successCount++;
- resolve();
- } else if (statusRes.status === 'failed') {
- clearInterval(poll);
- failCount++;
- resolve();
- }
- }).fail(() => {
- clearInterval(poll);
- failCount++;
- resolve();
- });
- }, 1000);
- });
-
- } catch (e) {
- console.error(e);
- failCount++;
- }
- }
-
- updateProgress(100, '批量采集完成');
-
- $('#deep-progress-area').hide();
- $('#deep-result-area').removeClass('hidden');
- $('#deep-close-btn').prop('disabled', false).text('关闭');
-
- $('#deep-result-icon').attr('class', 'fas fa-info-circle text-blue-500 text-lg');
- $('#deep-result-title').text('批量任务结束');
- $('#deep-result-msg').text(`成功: ${successCount}, 失败: ${failCount}`);
- $('#deep-summary-preview').text('批量任务不提供摘要预览,请到深采管理查看详情。');
-
- loadData();
- }
- </script>
- {% endblock %}
|