// 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.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);
}
}