Files
python_script/flask-dev-api/templates/data_manage.html
2026-06-16 03:30:57 +08:00

530 lines
21 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据管理</title>
<link rel="stylesheet" href="{{ url_for('static', filename='font-awesome.css') }}">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, sans-serif;
background: #f5f5f5;
color: #333;
padding: 16px;
}
.db-section {
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 10px;
}
.db-tabs {
display: flex;
gap: 0;
border-bottom: 2px solid #e0e0e0;
padding: 0 16px;
background: #fafafa;
border-radius: 10px 10px 0 0;
}
.db-tab {
padding: 10px 18px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
border: none;
background: none;
color: #666;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
transition: all 0.15s;
}
.db-tab.active { color: #0078d4; border-bottom-color: #0078d4; }
.db-tab:hover { color: #333; }
.db-tab .tab-count {
display: inline-block;
background: #e0e0e0;
color: #666;
font-size: 10px;
padding: 1px 6px;
border-radius: 8px;
margin-left: 4px;
}
.db-tab.active .tab-count { background: #e8f0fe; color: #0078d4; }
.db-toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
flex-wrap: wrap;
}
.db-search {
display: flex;
align-items: center;
background: #f5f5f5;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 5px 10px;
gap: 6px;
flex: 1;
min-width: 180px;
max-width: 300px;
}
.db-search:focus-within { border-color: #0078d4; background: #fff; }
.db-search i { color: #bbb; font-size: 12px; }
.db-search input {
border: none;
background: none;
outline: none;
font-size: 13px;
width: 100%;
color: #333;
}
.db-btn {
padding: 5px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
background: #fff;
color: #666;
transition: all 0.15s;
display: inline-flex;
align-items: center;
gap: 4px;
}
.db-btn:hover { background: #f5f5f5; color: #333; }
.db-btn.danger { color: #c62828; border-color: #f5b3b3; }
.db-btn.danger:hover { background: #fde8e8; }
.db-btn.primary { background: #0078d4; color: #fff; border-color: #0078d4; }
.db-btn.primary:hover { background: #006cbd; }
.db-table-wrap { overflow-x: auto; }
.db-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.db-table th {
text-align: left;
padding: 8px 12px;
color: #999;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 11px;
border-bottom: 1px solid #e0e0e0;
background: #fafafa;
white-space: nowrap;
}
.db-table td {
padding: 7px 12px;
border-bottom: 1px solid #f0f0f0;
color: #555;
max-width: 280px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.db-table tr:hover td { background: #f8f9fa; }
.db-table .col-check { width: 36px; text-align: center; }
.db-table .col-actions { width: 80px; text-align: center; white-space: nowrap; }
.db-table .action-btn {
background: none;
border: none;
cursor: pointer;
color: #999;
font-size: 13px;
padding: 2px 6px;
border-radius: 4px;
transition: all 0.15s;
}
.db-table .action-btn:hover { color: #0078d4; background: #e8f0fe; }
.db-table .action-btn.del:hover { color: #c62828; background: #fde8e8; }
.db-pagination {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
font-size: 12px;
color: #999;
border-top: 1px solid #f0f0f0;
}
.db-pagination .page-btns { display: flex; gap: 4px; }
.db-pagination .page-btn {
padding: 4px 10px;
border: 1px solid #ddd;
border-radius: 4px;
background: #fff;
color: #666;
cursor: pointer;
font-size: 12px;
}
.db-pagination .page-btn:hover { background: #f0f0f0; }
.db-pagination .page-btn.active { background: #0078d4; color: #fff; border-color: #0078d4; }
.db-pagination .page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.db-empty {
text-align: center;
padding: 40px;
color: #ccc;
font-size: 14px;
}
/* 弹窗 */
.modal-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.3);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal-overlay.show { display: flex; }
.modal-box {
background: #fff;
border-radius: 12px;
padding: 24px;
width: 480px;
max-width: 90vw;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 8px 30px rgba(0,0,0,0.12);
}
.modal-box h3 {
font-size: 16px;
margin-bottom: 16px;
}
.modal-form .form-row {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.modal-form .form-row label {
width: 100px;
font-size: 13px;
color: #666;
text-align: right;
padding-right: 12px;
flex-shrink: 0;
}
.modal-form .form-row input,
.modal-form .form-row select {
flex: 1;
padding: 7px 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 13px;
outline: none;
}
.modal-form .form-row input:focus,
.modal-form .form-row select:focus { border-color: #0078d4; }
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 16px;
}
</style>
</head>
<body>
<div class="db-section">
<div class="db-tabs">
<button class="db-tab active" data-table="api_calls" onclick="switchDbTab('api_calls', this)">
<i class="fas fa-chart-bar"></i> 接口调用记录 <span class="tab-count" id="cnt-api_calls">0</span>
</button>
<button class="db-tab" data-table="search_history" onclick="switchDbTab('search_history', this)">
<i class="fas fa-search"></i> 搜索历史 <span class="tab-count" id="cnt-search_history">0</span>
</button>
<button class="db-tab" data-table="recent_paths" onclick="switchDbTab('recent_paths', this)">
<i class="fas fa-folder"></i> 最近路径 <span class="tab-count" id="cnt-recent_paths">0</span>
</button>
</div>
<div class="db-toolbar">
<div class="db-search">
<i class="fas fa-search"></i>
<input type="text" id="dbSearchInput" placeholder="搜索..." oninput="onDbSearch()">
</div>
<button class="db-btn primary" onclick="openAddModal()"><i class="fas fa-folder-plus"></i> 新增</button>
<button class="db-btn danger" onclick="batchDelete()"><i class="fas fa-trash"></i> 删除选中</button>
<button class="db-btn danger" onclick="clearAll()"><i class="fas fa-trash"></i> 清空</button>
</div>
<div class="db-table-wrap">
<table class="db-table" id="db-table">
<thead id="db-thead"></thead>
<tbody id="db-tbody"></tbody>
</table>
</div>
<div class="db-empty" id="db-empty" style="display:none;">暂无数据</div>
<div class="db-pagination">
<span id="db-page-info">共 0 条</span>
<div class="page-btns" id="db-page-btns"></div>
</div>
</div>
<!-- 编辑弹窗 -->
<div class="modal-overlay" id="editModal">
<div class="modal-box">
<h3 id="modalTitle">新增记录</h3>
<div class="modal-form" id="modalForm"></div>
<div class="modal-actions">
<button class="db-btn" onclick="closeModal()">取消</button>
<button class="db-btn primary" onclick="saveRecord()">保存</button>
</div>
</div>
</div>
<script>
var dbCurrentTable = 'api_calls';
var dbCurrentPage = 1;
var dbPageSize = 20;
var dbSearchTimer = null;
var dbEditingId = null;
var TABLE_CONFIG = {
api_calls: {
label: '接口调用记录',
columns: [
{ key: 'id', label: 'ID', width: '50px', editable: false },
{ key: 'endpoint', label: '接口', editable: true },
{ key: 'method', label: '方法', editable: true, type: 'select', options: ['GET','POST','PUT','DELETE'] },
{ key: 'status_code', label: '状态码', editable: true, type: 'number' },
{ key: 'duration_ms', label: '耗时(ms)', editable: true, type: 'number' },
{ key: 'called_at', label: '调用时间', editable: true }
]
},
search_history: {
label: '搜索历史',
columns: [
{ key: 'id', label: 'ID', width: '50px', editable: false },
{ key: 'keyword', label: '关键词', editable: true },
{ key: 'root_path', label: '根路径', editable: true },
{ key: 'result_count', label: '结果数', editable: true, type: 'number' },
{ key: 'searched_at', label: '搜索时间', editable: true }
]
},
recent_paths: {
label: '最近路径',
columns: [
{ key: 'id', label: 'ID', width: '50px', editable: false },
{ key: 'path', label: '路径', editable: true },
{ key: 'last_used', label: '最后使用', editable: true }
]
}
};
function switchDbTab(table, btn) {
dbCurrentTable = table;
dbCurrentPage = 1;
document.getElementById('dbSearchInput').value = '';
document.querySelectorAll('.db-tab').forEach(function(t) { t.classList.remove('active'); });
btn.classList.add('active');
loadDbData();
}
function onDbSearch() {
clearTimeout(dbSearchTimer);
dbCurrentPage = 1;
dbSearchTimer = setTimeout(loadDbData, 300);
}
async function loadDbData() {
var search = document.getElementById('dbSearchInput').value.trim();
var url = '/api/db/' + dbCurrentTable + '?page=' + dbCurrentPage + '&page_size=' + dbPageSize;
if (search) url += '&search=' + encodeURIComponent(search);
try {
var res = await fetch(url);
var data = await res.json();
if (!data.success) return;
renderDbTable(data.rows, data.total);
renderDbPagination(data.total, data.page, data.page_size);
updateDbCounts();
} catch (e) { console.error('加载数据失败:', e); }
}
async function updateDbCounts() {
try {
var res = await fetch('/api/db/tables');
var data = await res.json();
data.tables.forEach(function(t) {
var el = document.getElementById('cnt-' + t.table);
if (el) el.textContent = t.count;
});
} catch (e) {}
}
function renderDbTable(rows, total) {
var cfg = TABLE_CONFIG[dbCurrentTable];
var thead = document.getElementById('db-thead');
var tbody = document.getElementById('db-tbody');
var emptyEl = document.getElementById('db-empty');
var ths = '<tr><th class="col-check"><input type="checkbox" id="dbCheckAll" onchange="toggleAllCheck(this)"></th>';
cfg.columns.forEach(function(c) {
ths += '<th' + (c.width ? ' style="width:' + c.width + '"' : '') + '>' + c.label + '</th>';
});
ths += '<th class="col-actions">操作</th></tr>';
thead.innerHTML = ths;
if (!rows.length) {
tbody.innerHTML = '';
emptyEl.style.display = 'block';
return;
}
emptyEl.style.display = 'none';
tbody.innerHTML = rows.map(function(row) {
var tds = '<tr><td class="col-check"><input type="checkbox" class="db-row-check" value="' + row.id + '"></td>';
cfg.columns.forEach(function(c) {
var v = row[c.key] != null ? row[c.key] : '';
tds += '<td title="' + escapeHtml(String(v)) + '">' + escapeHtml(String(v)) + '</td>';
});
tds += '<td class="col-actions">'
+ '<button class="action-btn" onclick="openEditModal(' + row.id + ')" title="编辑"><i class="fas fa-pen"></i></button> '
+ '<button class="action-btn del" onclick="deleteOne(' + row.id + ')" title="删除"><i class="fas fa-trash"></i></button>'
+ '</td></tr>';
return tds;
}).join('');
}
function escapeHtml(s) {
return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function renderDbPagination(total, page, pageSize) {
var totalPages = Math.ceil(total / pageSize) || 1;
document.getElementById('db-page-info').textContent = '共 ' + total + ' 条,第 ' + page + '/' + totalPages + ' 页';
var btns = '';
btns += '<button class="page-btn" onclick="goDbPage(1)" ' + (page <= 1 ? 'disabled' : '') + '>首页</button>';
btns += '<button class="page-btn" onclick="goDbPage(' + (page - 1) + ')" ' + (page <= 1 ? 'disabled' : '') + '>上一页</button>';
var start = Math.max(1, page - 2);
var end = Math.min(totalPages, start + 4);
for (var i = start; i <= end; i++) {
btns += '<button class="page-btn' + (i === page ? ' active' : '') + '" onclick="goDbPage(' + i + ')">' + i + '</button>';
}
btns += '<button class="page-btn" onclick="goDbPage(' + (page + 1) + ')" ' + (page >= totalPages ? 'disabled' : '') + '>下一页</button>';
btns += '<button class="page-btn" onclick="goDbPage(' + totalPages + ')" ' + (page >= totalPages ? 'disabled' : '') + '>末页</button>';
document.getElementById('db-page-btns').innerHTML = btns;
}
function goDbPage(p) {
dbCurrentPage = p;
loadDbData();
}
function toggleAllCheck(el) {
document.querySelectorAll('.db-row-check').forEach(function(cb) { cb.checked = el.checked; });
}
function openAddModal() {
dbEditingId = null;
document.getElementById('modalTitle').textContent = '新增记录';
buildModalForm(null);
document.getElementById('editModal').classList.add('show');
}
async function openEditModal(id) {
dbEditingId = id;
document.getElementById('modalTitle').textContent = '编辑记录 #' + id;
var search = document.getElementById('dbSearchInput').value.trim();
var url = '/api/db/' + dbCurrentTable + '?page=' + dbCurrentPage + '&page_size=' + dbPageSize;
if (search) url += '&search=' + encodeURIComponent(search);
try {
var res = await fetch(url);
var data = await res.json();
var row = data.rows.find(function(r) { return r.id === id; });
buildModalForm(row);
document.getElementById('editModal').classList.add('show');
} catch (e) {}
}
function buildModalForm(row) {
var cfg = TABLE_CONFIG[dbCurrentTable];
var html = '';
cfg.columns.forEach(function(c) {
if (c.key === 'id') return;
var val = row ? (row[c.key] != null ? row[c.key] : '') : '';
html += '<div class="form-row"><label>' + c.label + '</label>';
if (c.type === 'select') {
html += '<select data-col="' + c.key + '">';
c.options.forEach(function(opt) {
html += '<option value="' + opt + '"' + (val === opt ? ' selected' : '') + '>' + opt + '</option>';
});
html += '</select>';
} else {
html += '<input type="' + (c.type || 'text') + '" data-col="' + c.key + '" value="' + escapeHtml(String(val)) + '">';
}
html += '</div>';
});
document.getElementById('modalForm').innerHTML = html;
}
function closeModal() {
document.getElementById('editModal').classList.remove('show');
}
async function saveRecord() {
var cfg = TABLE_CONFIG[dbCurrentTable];
var data = {};
cfg.columns.forEach(function(c) {
if (c.key === 'id') return;
var el = document.querySelector('#modalForm [data-col="' + c.key + '"]');
if (el) data[c.key] = el.value;
});
var url, method;
if (dbEditingId) {
url = '/api/db/' + dbCurrentTable + '/' + dbEditingId;
method = 'PUT';
} else {
url = '/api/db/' + dbCurrentTable;
method = 'POST';
}
try {
var res = await fetch(url, { method: method, headers: {'Content-Type':'application/json'}, body: JSON.stringify(data) });
var result = await res.json();
if (result.success) {
closeModal();
loadDbData();
} else {
alert(result.error || '操作失败');
}
} catch (e) { alert('请求失败: ' + e.message); }
}
async function deleteOne(id) {
if (!confirm('确定删除记录 #' + id + '')) return;
try {
var res = await fetch('/api/db/' + dbCurrentTable + '/' + id, { method: 'DELETE' });
var data = await res.json();
if (data.success) loadDbData();
} catch (e) {}
}
async function batchDelete() {
var ids = [];
document.querySelectorAll('.db-row-check:checked').forEach(function(cb) { ids.push(parseInt(cb.value)); });
if (!ids.length) { alert('请先选择要删除的记录'); return; }
if (!confirm('确定删除选中的 ' + ids.length + ' 条记录?')) return;
try {
var res = await fetch('/api/db/' + dbCurrentTable + '/delete-batch', {
method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ids: ids})
});
var data = await res.json();
if (data.success) loadDbData();
} catch (e) {}
}
async function clearAll() {
var cfg = TABLE_CONFIG[dbCurrentTable];
if (!confirm('确定清空「' + cfg.label + '」的所有数据?此操作不可撤销!')) return;
try {
var res = await fetch('/api/db/' + dbCurrentTable + '/clear', { method: 'POST' });
var data = await res.json();
if (data.success) loadDbData();
} catch (e) {}
}
loadDbData();
</script>
</body>
</html>