2025-11-30 12:36:48 +08:00
<!DOCTYPE html>
< html lang = "zh-CN" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > SQLite数据库查看器< / title >
< style >
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
.container {
width: 100%;
margin: 0 auto;
padding: 20px;
}
.header {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 25px;
margin-bottom: 25px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.header h1 {
color: #2c3e50;
font-size: 2.5em;
margin-bottom: 10px;
text-align: center;
}
.controls {
display: flex;
gap: 20px;
align-items: center;
flex-wrap: wrap;
margin-top: 20px;
}
.control-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.control-group label {
font-weight: 600;
color: #34495e;
font-size: 0.9em;
}
select, input {
padding: 12px 15px;
border: 2px solid #e0e6ed;
border-radius: 8px;
font-size: 14px;
transition: all 0.3s ease;
background: white;
}
select:focus, input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.btn {
padding: 12px 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.data-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 25px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.table-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
}
.table-info h2 {
color: #2c3e50;
font-size: 1.5em;
}
.stats {
display: flex;
gap: 20px;
font-size: 0.9em;
color: #7f8c8d;
}
.table-wrapper {
overflow-x: auto;
border-radius: 10px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
table {
width: 100%;
border-collapse: collapse;
background: white;
font-size: 14px;
}
th {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 12px;
text-align: left;
font-weight: 600;
position: sticky;
top: 0;
z-index: 10;
}
td {
padding: 12px;
border-bottom: 1px solid #ecf0f1;
vertical-align: top;
}
tr:nth-child(even) {
background-color: #f8f9fa;
}
tr:hover {
background-color: #e3f2fd;
transition: background-color 0.3s ease;
}
.multiline-cell {
white-space: pre-wrap;
line-height: 1.6;
max-height: 200px;
overflow-y: auto;
padding: 8px;
background: #fff3cd;
border-radius: 6px;
border-left: 4px solid #ffc107;
}
.normal-cell {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 300px;
}
.empty-cell {
color: #95a5a6;
font-style: italic;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
margin-top: 25px;
flex-wrap: wrap;
}
.page-info {
color: #7f8c8d;
font-weight: 600;
}
.page-btn {
padding: 8px 12px;
border: 2px solid #e0e6ed;
background: white;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
min-width: 40px;
text-align: center;
}
.page-btn:hover {
border-color: #667eea;
background: #667eea;
color: white;
}
.page-btn.active {
background: #667eea;
color: white;
border-color: #667eea;
}
.page-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.loading {
text-align: center;
padding: 40px;
color: #7f8c8d;
font-size: 1.1em;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 8px;
border: 1px solid #f5c6cb;
margin: 20px 0;
}
.no-data {
text-align: center;
padding: 40px;
color: #7f8c8d;
font-size: 1.1em;
}
.analyze-btn {
background: linear-gradient(135deg, #28a745, #20c997);
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
font-size: 1em;
cursor: pointer;
transition: all 0.3s ease;
}
.analyze-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(40, 167, 69, 0.3);
}
.analyze-btn:disabled {
background: #6c757d;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.progress-container {
margin-top: 20px;
padding: 15px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
display: none;
}
.progress-bar {
width: 100%;
height: 20px;
background: #e9ecef;
border-radius: 10px;
overflow: hidden;
margin: 10px 0;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
transition: width 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.8em;
font-weight: 600;
}
@media (max-width: 768px) {
.controls {
flex-direction: column;
align-items: stretch;
}
.table-info {
flex-direction: column;
gap: 15px;
text-align: center;
}
.stats {
justify-content: center;
}
}
< / style >
< / head >
< body >
< div class = "container" >
< div class = "header" >
< h1 > 🗄️ SQLite数据库查看器< / h1 >
< div class = "controls" >
< div class = "control-group" >
< label for = "tableSelect" > 选择数据表:< / label >
< select id = "tableSelect" >
< option value = "" > 加载中...< / option >
< / select >
< / div >
< div class = "control-group" >
< label for = "analyzeBtn" > 分析:< / label >
< button id = "analyzeScoresBtn" class = "analyze-btn" > 📊 分析缺失分数< / button >
< / div >
< div class = "control-group" >
< label for = "searchField" > 筛选字段:< / label >
< select id = "searchField" multiple disabled style = "min-height: 80px;" >
< option value = "" > 所有文本字段< / option >
< / select >
< / div >
< div class = "control-group" >
< label for = "searchValue" > 筛选内容:< / label >
< input type = "text" id = "searchValue" placeholder = "输入筛选内容..." disabled >
< / div >
< button class = "btn" onclick = "loadData()" > 刷新数据< / button >
< / div >
< / div >
< / div >
< div class = "data-container" >
< div class = "table-info" >
< h2 id = "tableName" > 请选择数据表< / h2 >
< div class = "stats" >
< span id = "recordCount" > 记录数: 0< / span >
< span id = "pageInfo" > 第 0 页,共 0 页< / span >
< / div >
< / div >
< div id = "dataContainer" >
< div class = "no-data" > 请选择数据表以查看内容< / div >
< / div >
< div id = "pagination" class = "pagination" style = "display: none;" >
< button class = "page-btn" onclick = "changePage('prev')" id = "prevBtn" > 上一页< / button >
< span class = "page-info" id = "pageInfoDetail" > < / span >
< button class = "page-btn" onclick = "changePage('next')" id = "nextBtn" > 下一页< / button >
< / div >
< / div >
< / div >
< div id = "progressSection" class = "progress-container" >
< h3 > 📊 分数分析进度< / h3 >
< div class = "progress-bar" >
< div id = "progressFill" class = "progress-fill" style = "width: 0%;" > 0%< / div >
< / div >
< p id = "progressText" > 等待分析开始...< / p >
< / div >
< / div >
< script >
let currentTable = '';
let currentPage = 1;
let perPage = 50;
let totalPages = 1;
let currentData = null;
// 绑定事件
document.addEventListener('DOMContentLoaded', function() {
loadTables();
// 绑定事件
document.getElementById('tableSelect').addEventListener('change', function() {
currentTable = this.value;
currentPage = 1;
if (currentTable) {
loadTableStructure();
loadData();
}
});
// 分析缺失分数按钮事件
document.getElementById('analyzeScoresBtn').addEventListener('click', analyzeMissingScores);
});
// 分析缺失分数
async function analyzeMissingScores() {
const analyzeBtn = document.getElementById('analyzeScoresBtn');
const progressSection = document.getElementById('progressSection');
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
try {
// 禁用按钮
analyzeBtn.disabled = true;
analyzeBtn.textContent = '分析中...';
// 显示进度条
progressSection.style.display = 'block';
progressFill.style.width = '0%';
progressFill.textContent = '0%';
progressText.textContent = '正在启动分析任务...';
// 启动分析任务
const response = await fetch('/api/analyze_missing_scores');
const data = await response.json();
if (data.task_id) {
// 定期查询任务状态
const interval = setInterval(async () => {
try {
const statusResponse = await fetch(`/api/update_task_status/${data.task_id}`);
const statusData = await statusResponse.json();
// 更新进度
progressFill.style.width = `${statusData.progress}%`;
progressFill.textContent = `${statusData.progress}%`;
if (statusData.status === 'running') {
progressText.textContent = `正在分析: ${statusData.completed}/${statusData.total} 个产品`;
} else if (statusData.status === 'completed') {
progressText.textContent = '🎉 所有缺失分数分析完成!';
clearInterval(interval);
analyzeBtn.disabled = false;
analyzeBtn.textContent = '📊 分析缺失分数';
// 如果当前正在查看product_analysis表, 自动刷新
if (currentTable === 'product_analysis') {
loadData();
}
} else if (statusData.status === 'failed') {
progressText.textContent = `❌ 分析失败: ${statusData.error}`;
clearInterval(interval);
analyzeBtn.disabled = false;
analyzeBtn.textContent = '📊 分析缺失分数';
}
} catch (error) {
console.error('查询任务状态失败:', error);
progressText.textContent = '查询任务状态失败';
}
}, 2000);
}
} catch (error) {
console.error('启动分析任务失败:', error);
progressText.textContent = `启动分析失败: ${error.message}`;
analyzeBtn.disabled = false;
analyzeBtn.textContent = '📊 分析缺失分数';
}
2025-12-01 21:07:30 +08:00
}
2025-11-30 12:36:48 +08:00
document.getElementById('searchField').addEventListener('change', loadData);
document.getElementById('searchValue').addEventListener('input', debounce(loadData, 500));
// 防抖函数
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 加载表列表
async function loadTables() {
try {
const response = await fetch('/api/tables');
const data = await response.json();
const select = document.getElementById('tableSelect');
select.innerHTML = '< option value = "" > 选择数据表...< / option > ';
data.tables.forEach(table => {
const option = document.createElement('option');
option.value = table;
option.textContent = table;
select.appendChild(option);
});
} catch (error) {
console.error('加载表列表失败:', error);
showError('加载表列表失败: ' + error.message);
}
}
// 加载表结构
async function loadTableStructure() {
if (!currentTable) return;
try {
const response = await fetch(`/api/table/${currentTable}/structure`);
const data = await response.json();
const searchField = document.getElementById('searchField');
searchField.innerHTML = '< option value = "" > 所有文本字段< / option > ';
data.structure.forEach(field => {
const option = document.createElement('option');
option.value = field.name;
option.textContent = field.name;
searchField.appendChild(option);
});
searchField.disabled = false;
document.getElementById('searchValue').disabled = false;
} catch (error) {
console.error('加载表结构失败:', error);
}
}
// 加载数据
async function loadData() {
if (!currentTable) return;
const container = document.getElementById('dataContainer');
container.innerHTML = '< div class = "loading" > 📊 数据加载中...< / div > ';
const searchFieldSelect = document.getElementById('searchField');
const searchValue = document.getElementById('searchValue').value;
try {
let url = `/api/table/${currentTable}/data?page=${currentPage}&per_page=${perPage}`;
if (searchValue) {
// 获取所有选中的字段
const selectedFields = Array.from(searchFieldSelect.selectedOptions)
.map(option => option.value)
.filter(value => value !== '');
if (selectedFields.length > 0) {
// 如果选择了特定字段,传递所有选中的字段
selectedFields.forEach(field => {
url += `&search_field=${encodeURIComponent(field)}`;
});
} else {
// 否则使用"all"表示所有文本字段
url += '&search_field=all';
}
url += `&search_value=${encodeURIComponent(searchValue)}`;
}
const response = await fetch(url);
currentData = await response.json();
displayData(currentData);
updatePagination();
} catch (error) {
console.error('加载数据失败:', error);
showError('加载数据失败: ' + error.message);
}
}
// 显示数据
function displayData(data) {
const container = document.getElementById('dataContainer');
if (!data.rows || data.rows.length === 0) {
container.innerHTML = '< div class = "no-data" > 📭 没有找到数据< / div > ';
return;
}
let html = '< div class = "table-wrapper" > < table > < thead > < tr > ';
// 表头
data.columns.forEach(col => {
html += `< th > ${col}< / th > `;
});
html += '< / tr > < / thead > < tbody > ';
// 数据行
data.rows.forEach(row => {
html += '< tr > ';
2025-12-01 21:07:30 +08:00
row.forEach((cell, index) => {
const colName = data.columns[index];
2025-11-30 12:36:48 +08:00
if (cell.type === 'multiline') {
html += `< td > < div class = "multiline-cell" > ${escapeHtml(cell.value)}< / div > < / td > `;
} else if (cell.type === 'empty') {
html += '< td > < div class = "empty-cell" > 空< / div > < / td > ';
2025-12-01 21:07:30 +08:00
} else if (colName === 'product_link' & & cell.value) {
// 渲染为链接
html += `< td > < div class = "normal-cell" > < a href = "${escapeHtml(cell.value)}" target = "_blank" rel = "noopener noreferrer" > ${escapeHtml(cell.value)}< / a > < / div > < / td > `;
2025-11-30 12:36:48 +08:00
} else {
html += `< td > < div class = "normal-cell" > ${escapeHtml(cell.value)}< / div > < / td > `;
}
});
html += '< / tr > ';
});
html += '< / tbody > < / table > < / div > ';
container.innerHTML = html;
// 更新统计信息
document.getElementById('tableName').textContent = `📋 ${currentTable}`;
document.getElementById('recordCount').textContent = `记录数: ${data.total_count}`;
document.getElementById('pageInfo').textContent = `第 ${currentPage} 页,共 ${data.total_pages} 页`;
}
// 更新分页
function updatePagination() {
if (!currentData) return;
totalPages = currentData.total_pages;
const pagination = document.getElementById('pagination');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const pageInfo = document.getElementById('pageInfoDetail');
if (totalPages < = 1) {
pagination.style.display = 'none';
return;
}
pagination.style.display = 'flex';
prevBtn.disabled = currentPage < = 1;
nextBtn.disabled = currentPage >= totalPages;
pageInfo.textContent = `${currentPage} / ${totalPages}`;
}
// 翻页
function changePage(direction) {
if (direction === 'prev' & & currentPage > 1) {
currentPage--;
loadData();
} else if (direction === 'next' & & currentPage < totalPages ) {
currentPage++;
loadData();
}
}
// HTML转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 显示错误
function showError(message) {
const container = document.getElementById('dataContainer');
container.innerHTML = `< div class = "error" > ❌ ${message}< / div > `;
}
< / script >
< / body >
< / html >