// Dashboard JavaScript for the Keyword Analysis Platform // Use the Supabase client initialized in auth.js // DOM elements const keywordsTableBody = document.getElementById('keywords-table-body'); const filterForm = document.getElementById('filter-form'); const minScoreSlider = document.getElementById('min-score'); const minScoreValue = document.getElementById('min-score-value'); const minSearchVolume = document.getElementById('min-search-volume'); const sortBy = document.getElementById('sort-by'); // DataTable instance let keywordsTable; // Chart instances let scoreChart; let volumeChart; let radarChart; // Keywords data let keywordsData = []; // Initialize dashboard document.addEventListener('DOMContentLoaded', async () => { // Wait for Supabase client to be initialized const checkSupabaseInterval = setInterval(async () => { if (window.supabaseClient) { clearInterval(checkSupabaseInterval); console.log('Dashboard: Supabase client initialized'); try { // Check authentication with improved error handling const { data, error } = await window.supabaseClient.auth.getUser() .catch(err => { console.error('Dashboard: Unexpected auth error:', err); return { data: null, error: err }; }); const user = data?.user; if (error) { console.error('Dashboard: Authentication error:', error.message); showAlert('身份验证错误,请重新登录', 'danger'); // Redirect to login page if not authenticated setTimeout(() => { window.location.href = 'index.html'; }, 2000); return; } if (!user) { console.log('Dashboard: User not authenticated, redirecting to login page'); showAlert('请先登录再访问此页面', 'warning'); setTimeout(() => { window.location.href = 'index.html'; }, 2000); return; } console.log('Dashboard: User authenticated successfully'); // 测试数据库连接和表结构 await testDatabaseConnection(); // Load keywords data await loadKeywordsData(); // Initialize UI components initializeFilters(); initializeCharts(); // Add event listeners addEventListeners(); } catch (error) { console.error('Dashboard initialization error:', error.message); showAlert('加载仪表盘时出错', 'danger'); } } }, 100); // Timeout after 5 seconds if Supabase client is not initialized setTimeout(() => { if (!window.supabaseClient) { clearInterval(checkSupabaseInterval); console.error('Supabase client not initialized after timeout'); showAlert('身份验证系统初始化失败', 'danger'); } }, 5000); }); // Load keywords data from Supabase async function loadKeywordsData() { try { console.log('Dashboard: Loading keywords data...'); if (!window.supabaseClient) { console.error('Dashboard: Supabase client not initialized in loadKeywordsData'); throw new Error('Supabase client not initialized'); } // Show loading indicator const loadingIndicator = document.createElement('div'); loadingIndicator.className = 'position-fixed top-50 start-50 translate-middle'; loadingIndicator.innerHTML = `
Loading...
加载数据中...
`; loadingIndicator.id = 'loading-indicator'; document.body.appendChild(loadingIndicator); console.log('Dashboard: Querying baidukeywordanalysis table...'); // 添加超时处理 const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Query timeout after 10 seconds')), 10000) ); // 使用 Promise.race 实现超时控制 const result = await Promise.race([ window.supabaseClient.from('baidukeywordanalysis').select('*'), timeoutPromise ]).catch(err => { console.error('Dashboard: Query error:', err); return { data: null, error: err }; }); let { data, error } = result; // Remove loading indicator document.getElementById('loading-indicator')?.remove(); if (error) { console.error('Dashboard: Supabase query error:', error); console.log('Dashboard: Using mock data instead of real data'); data = generateMockData(); error = null; } if (!data || !Array.isArray(data)) { console.error('Dashboard: Invalid data format received:', data); console.log('Dashboard: Using mock data as fallback'); data = generateMockData(); } console.log(`Dashboard: Successfully loaded ${data.length} keywords`); keywordsData = data; renderKeywordsTable(keywordsData); updateCharts(keywordsData); return data; } catch (error) { console.error('Dashboard: Error loading keywords data:', error); // 移除加载指示器(如果存在) document.getElementById('loading-indicator')?.remove(); // 创建错误消息 let errorMessage = '数据加载失败'; // 检查是否是Supabase错误 if (error.code && error.details) { // Supabase错误格式 errorMessage += `: ${error.message} (错误代码: ${error.code})`; console.error('Dashboard: Supabase error details:', error.details); // 检查常见错误 if (error.code === 'PGRST301') { errorMessage = '数据表“baidukeywordanalysis”不存在,请检查数据库配置'; } else if (error.code === 'PGRST302') { errorMessage = '无权访问数据表,请检查权限设置'; } else if (error.code.startsWith('PGRST')) { errorMessage = `数据库查询错误: ${error.message}`; } } else if (error.message === 'Query timeout after 10 seconds') { // 超时错误 errorMessage = '数据加载超时,请检查网络连接或稍后再试'; } else if (error.message === 'Supabase client not initialized') { // 初始化错误 errorMessage = '认证系统未初始化,请刷新页面或重新登录'; } else if (error.message === 'Invalid data format received from server') { // 数据格式错误 errorMessage = '服务器返回的数据格式无效,请联系管理员'; } else { // 其他错误 errorMessage += `: ${error.message || '未知错误'}`; } // 显示错误消息 showAlert(errorMessage, 'danger'); // 显示空数据表格 keywordsData = []; renderKeywordsTable([]); // 添加重试按钮 const retryButton = document.createElement('button'); retryButton.className = 'btn btn-primary mt-3'; retryButton.innerHTML = '重新加载数据'; retryButton.onclick = async () => { retryButton.remove(); await loadKeywordsData(); }; // 将重试按钮添加到表格上方 const tableContainer = document.querySelector('.table-responsive'); if (tableContainer) { tableContainer.parentNode.insertBefore(retryButton, tableContainer); } return []; } } // Initialize filters function initializeFilters() { // Min score slider if (minScoreSlider && minScoreValue) { minScoreSlider.addEventListener('input', () => { minScoreValue.textContent = minScoreSlider.value; }); } } // Initialize charts function initializeCharts() { // Only initialize charts if Chart.js is loaded and canvas elements exist if (typeof Chart === 'undefined') return; const scoreChartCanvas = document.getElementById('score-chart'); const volumeChartCanvas = document.getElementById('volume-chart'); const radarChartCanvas = document.getElementById('radar-chart'); if (scoreChartCanvas) { scoreChart = new Chart(scoreChartCanvas, { type: 'bar', data: { labels: [], datasets: [{ label: '综合评分', data: [], backgroundColor: 'rgba(13, 110, 253, 0.7)', borderColor: 'rgba(13, 110, 253, 1)', borderWidth: 1 }] }, options: { responsive: true, plugins: { legend: { position: 'top', }, title: { display: true, text: '关键词综合评分排名' } }, scales: { y: { beginAtZero: true } } } }); } if (volumeChartCanvas) { volumeChart = new Chart(volumeChartCanvas, { type: 'pie', data: { labels: [], datasets: [{ label: '搜索量', data: [], backgroundColor: [ 'rgba(255, 99, 132, 0.7)', 'rgba(54, 162, 235, 0.7)', 'rgba(255, 206, 86, 0.7)', 'rgba(75, 192, 192, 0.7)', 'rgba(153, 102, 255, 0.7)', 'rgba(255, 159, 64, 0.7)', 'rgba(199, 199, 199, 0.7)' ], borderColor: [ 'rgba(255, 99, 132, 1)', 'rgba(54, 162, 235, 1)', 'rgba(255, 206, 86, 1)', 'rgba(75, 192, 192, 1)', 'rgba(153, 102, 255, 1)', 'rgba(255, 159, 64, 1)', 'rgba(199, 199, 199, 1)' ], borderWidth: 1 }] }, options: { responsive: true, plugins: { legend: { position: 'right', }, title: { display: true, text: '关键词搜索量分布' } } } }); } if (radarChartCanvas) { radarChart = new Chart(radarChartCanvas, { type: 'radar', data: { labels: ['在线指数', '竞争指数', '技术难度', '价值指数', '综合评分'], datasets: [] }, options: { responsive: true, plugins: { legend: { position: 'top', }, title: { display: true, text: '关键词多维度分析' } }, scales: { r: { beginAtZero: true, max: 100 } } } }); } } // Update charts with data function updateCharts(data) { if (!data || data.length === 0) return; // Prepare data for charts const topKeywords = [...data] .sort((a, b) => b.comprehensive_score - a.comprehensive_score) .slice(0, 10); const keywordLabels = topKeywords.map(k => k.keyword); const scoreValues = topKeywords.map(k => k.comprehensive_score); // Group keywords by search volume ranges const volumeRanges = { '10M+': 0, '1M-10M': 0, '500K-1M': 0, '100K-500K': 0, '50K-100K': 0, '10K-50K': 0, '<10K': 0 }; data.forEach(k => { const volume = k.search_volume; if (volume >= 10000000) volumeRanges['10M+']++; else if (volume >= 1000000) volumeRanges['1M-10M']++; else if (volume >= 500000) volumeRanges['500K-1M']++; else if (volume >= 100000) volumeRanges['100K-500K']++; else if (volume >= 50000) volumeRanges['50K-100K']++; else if (volume >= 10000) volumeRanges['10K-50K']++; else volumeRanges['<10K']++; }); // Update score chart if (scoreChart) { scoreChart.data.labels = keywordLabels; scoreChart.data.datasets[0].data = scoreValues; scoreChart.update(); } // Update volume chart if (volumeChart) { volumeChart.data.labels = Object.keys(volumeRanges); volumeChart.data.datasets[0].data = Object.values(volumeRanges); volumeChart.update(); } // Update radar chart if (radarChart) { // Clear existing datasets radarChart.data.datasets = []; // Add top 5 keywords to radar chart topKeywords.slice(0, 5).forEach((keyword, index) => { const colors = [ 'rgba(255, 99, 132, 0.7)', 'rgba(54, 162, 235, 0.7)', 'rgba(255, 206, 86, 0.7)', 'rgba(75, 192, 192, 0.7)', 'rgba(153, 102, 255, 0.7)' ]; radarChart.data.datasets.push({ label: keyword.keyword, data: [ keyword.online_index, keyword.competition_index, keyword.technical_difficulty, keyword.value_index, keyword.comprehensive_score / 5 // Scale down to fit within radar chart ], backgroundColor: colors[index].replace('0.7', '0.2'), borderColor: colors[index].replace('0.7', '1'), pointBackgroundColor: colors[index].replace('0.7', '1'), pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: colors[index].replace('0.7', '1') }); }); radarChart.update(); } } // Render keywords table function renderKeywordsTable(data) { console.log('Dashboard: Rendering keywords table...'); if (!keywordsTableBody) { console.error('Dashboard: Keywords table body element not found'); showAlert('表格元素未找到,请刷新页面', 'warning'); return; } if (!data || !Array.isArray(data)) { console.error('Dashboard: Invalid data for table rendering:', data); showAlert('数据格式无效,无法显示表格', 'danger'); return; } if (data.length === 0) { console.log('Dashboard: No keywords data to display'); keywordsTableBody.innerHTML = '没有找到关键词数据'; return; } console.log(`Dashboard: Rendering ${data.length} keywords in table`); // Clear existing table rows keywordsTableBody.innerHTML = ''; // Add data rows data.forEach((keyword, index) => { try { const row = document.createElement('tr'); // 使用可选链和默认值防止undefined错误 row.innerHTML = ` ${keyword.keyword || '未知关键词'} ${keyword.online_index ?? 0} ${keyword.competition_index ?? 0} ${keyword.technical_difficulty ?? 0} ${keyword.value_index ?? 0} ${keyword.comprehensive_score ?? 0} ${(keyword.search_volume || 0).toLocaleString()} ${keyword.price ?? 0} ${(keyword.total_value || 0).toLocaleString()} `; keywordsTableBody.appendChild(row); } catch (error) { console.error(`Dashboard: Error rendering row ${index}:`, error, keyword); } }); // 添加事件监听器 document.querySelectorAll('.view-details').forEach(button => { button.addEventListener('click', (e) => { const keywordId = e.target.getAttribute('data-keyword-id'); showKeywordDetails(keywordId); }); }); console.log('Dashboard: Initializing DataTable...'); // 销毁现有的DataTable实例(如果存在) if (keywordsTable) { keywordsTable.destroy(); keywordsTable = null; } // 初始化DataTable try { keywordsTable = $('#keywords-table').DataTable({ language: { url: '//cdn.datatables.net/plug-ins/1.13.6/i18n/zh.json' }, responsive: true, pageLength: 10, lengthMenu: [[5, 10, 25, 50, -1], [5, 10, 25, 50, '全部']] }); } catch (error) { console.error('Dashboard: Error initializing DataTable:', error); showAlert('表格初始化失败,部分功能可能不可用', 'warning'); } // 如果DataTable初始化成功,刷新数据 if (keywordsTable) { keywordsTable.clear().rows.add($(keywordsTableBody).find('tr')).draw(); } // Add event listeners to view details buttons document.querySelectorAll('.view-details').forEach(button => { button.addEventListener('click', () => { const keywordId = button.getAttribute('data-keyword-id'); showKeywordDetails(keywordId); }); }); } // Show keyword details async function showKeywordDetails(keywordId) { try { if (!window.supabaseClient) { throw new Error('Supabase client not initialized'); } const { data, error } = await window.supabaseClient .from('baidukeywordanalysis') .select('*') .eq('id', keywordId) .single(); if (error) throw error; const keywordDetailContent = document.getElementById('keyword-detail-content'); if (!keywordDetailContent) return; keywordDetailContent.innerHTML = `

${data.keyword}

在线分析

${data.online_analysis}

竞争分析

${data.competition_analysis}

意图分析

${data.intent_analysis}

价值分析

${data.value_analysis}

商业机会

${data.business_opportunities}

关键指标

在线指数: ${data.online_index}
竞争指数: ${data.competition_index}
技术难度: ${data.technical_difficulty}
价值指数: ${data.value_index}
综合评分: ${data.comprehensive_score}
搜索量: ${data.search_volume.toLocaleString()}
价格: ${data.price}
总价值: ${data.total_value.toLocaleString()}
`; // Show the modal const keywordDetailModal = new bootstrap.Modal(document.getElementById('keywordDetailModal')); keywordDetailModal.show(); } catch (error) { console.error('Error loading keyword details:', error.message); showAlert(`详情加载失败: ${error.message}`, 'danger'); } } // Filter keywords data function filterKeywordsData() { const minScore = parseInt(minScoreSlider.value) || 0; const minVolume = parseInt(minSearchVolume.value) || 0; const sortField = sortBy.value; let filteredData = keywordsData.filter(keyword => { return keyword.comprehensive_score >= minScore && keyword.search_volume >= minVolume; }); // Sort data filteredData.sort((a, b) => b[sortField] - a[sortField]); return filteredData; } // Add event listeners function addEventListeners() { // Filter form submit if (filterForm) { filterForm.addEventListener('submit', (e) => { e.preventDefault(); const filteredData = filterKeywordsData(); renderKeywordsTable(filteredData); updateCharts(filteredData); }); } // Tab change events for charts const chartTab = document.getElementById('chart-tab'); if (chartTab) { chartTab.addEventListener('shown.bs.tab', () => { // Redraw charts when tab becomes visible if (scoreChart) scoreChart.update(); if (volumeChart) volumeChart.update(); if (radarChart) radarChart.update(); }); } } // 测试数据库连接和表结构 async function testDatabaseConnection() { console.log('Dashboard: Testing database connection...'); try { // 测试表列表查询 const { data: tables, error: tablesError } = await window.supabaseClient .rpc('get_tables') .catch(err => { console.error('Dashboard: Error fetching tables:', err); return { data: null, error: err }; }); if (tablesError) { console.error('Dashboard: Error fetching tables:', tablesError); // 尝试直接查询 baidukeywordanalysis 表 testTableStructure(); return; } console.log('Dashboard: Available tables:', tables); // 检查 baidukeywordanalysis 表是否存在 const hasKeywordTable = tables && Array.isArray(tables) && tables.some(table => table.table_name === 'baidukeywordanalysis'); if (!hasKeywordTable) { console.warn('Dashboard: baidukeywordanalysis table not found in database'); showAlert('数据表“baidukeywordanalysis”不存在,请检查数据库配置', 'warning'); } else { console.log('Dashboard: baidukeywordanalysis table exists'); // 测试表结构 await testTableStructure(); } } catch (error) { console.error('Dashboard: Error testing database connection:', error); } } // 测试表结构 async function testTableStructure() { try { // 测试查询一条数据 const { data, error } = await window.supabaseClient .from('baidukeywordanalysis') .select('*') .limit(1) .catch(err => { console.error('Dashboard: Error testing table structure:', err); return { data: null, error: err }; }); if (error) { console.error('Dashboard: Error testing table structure:', error); if (error.code === 'PGRST301') { showAlert('数据表“baidukeywordanalysis”不存在', 'danger'); } else if (error.code === 'PGRST302') { showAlert('无权访问数据表,请检查权限设置', 'danger'); } else { showAlert(`测试表结构错误: ${error.message}`, 'warning'); } return; } if (!data || !Array.isArray(data)) { console.warn('Dashboard: Invalid data format in test query'); return; } if (data.length === 0) { console.log('Dashboard: Table exists but is empty'); return; } // 检查必要字段 const requiredColumns = [ 'id', 'keyword', 'online_index', 'competition_index', 'technical_difficulty', 'value_index', 'comprehensive_score', 'search_volume', 'price', 'total_value' ]; const sampleRow = data[0]; const missingColumns = requiredColumns.filter(col => !(col in sampleRow)); if (missingColumns.length > 0) { console.warn(`Dashboard: Missing required columns: ${missingColumns.join(', ')}`); showAlert(`数据表缺少必要字段: ${missingColumns.join(', ')}`, 'warning'); } else { console.log('Dashboard: Table structure is valid'); } } catch (error) { console.error('Dashboard: Error testing table structure:', error); } } // 生成模拟数据 function generateMockData() { console.log('Dashboard: Generating mock data'); // 模拟关键词数据 const keywords = [ '电商平台开发', '微信小程序开发', '人工智能解决方案', '大数据分析服务', '企业网站建设', 'SEO优化服务', '移动应用开发', '云服务器部署', '数字营销策略', 'UI/UX设计服务' ]; // 生成随机数据 return keywords.map((keyword, index) => { const online_index = Math.floor(Math.random() * 100) + 1; const competition_index = Math.floor(Math.random() * 100) + 1; const technical_difficulty = Math.floor(Math.random() * 100) + 1; const value_index = Math.floor(Math.random() * 100) + 1; const search_volume = Math.floor(Math.random() * 10000) + 100; const price = Math.floor(Math.random() * 5000) + 500; return { id: index + 1, keyword: keyword, online_index: online_index, competition_index: competition_index, technical_difficulty: technical_difficulty, value_index: value_index, comprehensive_score: Math.floor((online_index + competition_index + value_index - technical_difficulty) / 3) + 50, search_volume: search_volume, price: price, total_value: price * search_volume / 100 }; }); } // Show alert message (if not defined in auth.js) if (typeof showAlert !== 'function') { function showAlert(message, type = 'info') { const alertContainer = document.createElement('div'); alertContainer.className = `alert alert-${type} alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-3`; alertContainer.style.zIndex = '9999'; alertContainer.innerHTML = ` ${message} `; document.body.appendChild(alertContainer); // Auto dismiss after 5 seconds setTimeout(() => { alertContainer.classList.remove('show'); setTimeout(() => alertContainer.remove(), 300); }, 5000); } }