Files
check_in/templates/data_base.html
jeremygan2021 7caff4d72d new
2026-03-20 16:18:58 +08:00

871 lines
28 KiB
HTML
Raw 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>数据库管理 - 云南AI共生大会</title>
<style>
:root {
--primary-color: #00f2ff;
--secondary-color: #0066ff;
--bg-color: #050814;
--card-bg: rgba(12, 24, 50, 0.5);
--text-color: #ffffff;
--text-muted: #b0c4de;
--accent-glow: rgba(0, 242, 255, 0.6);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
background-image:
radial-gradient(circle at 50% 0%, #1a3a75 0%, #050814 60%),
radial-gradient(circle at 85% 30%, rgba(0, 242, 255, 0.1) 0%, transparent 40%),
radial-gradient(circle at 15% 70%, rgba(0, 102, 255, 0.15) 0%, transparent 40%);
background-attachment: fixed;
min-height: 100vh;
display: flex;
flex-direction: column;
padding: 20px;
}
.container {
width: 100%;
max-width: 1400px;
margin: 0 auto;
position: relative;
z-index: 1;
}
h1 {
font-size: 1.8rem;
margin-bottom: 20px;
text-align: center;
background: linear-gradient(180deg, #ffffff 0%, #d0eaff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 900;
text-shadow: 0 0 20px rgba(0, 242, 255, 0.4);
}
.nav-bar {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.nav-bar a {
text-decoration: none;
color: var(--text-muted);
padding: 8px 16px;
border-radius: 6px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(0, 242, 255, 0.2);
transition: all 0.3s;
}
.nav-bar a:hover, .nav-bar a.active {
background: rgba(0, 242, 255, 0.1);
border-color: var(--primary-color);
color: var(--primary-color);
}
.card {
background: var(--card-bg);
border-radius: 16px;
padding: 20px;
margin-bottom: 20px;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(0, 242, 255, 0.2);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.5);
}
.table-list {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 20px;
}
.table-btn {
padding: 10px 20px;
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(0, 242, 255, 0.3);
border-radius: 8px;
color: white;
cursor: pointer;
transition: all 0.3s;
}
.table-btn:hover, .table-btn.active {
background: linear-gradient(90deg, #0051ff 0%, #00f2ff 100%);
border-color: var(--primary-color);
}
.toolbar {
display: flex;
gap: 10px;
margin-bottom: 15px;
flex-wrap: wrap;
align-items: center;
}
.toolbar input[type="text"] {
padding: 8px 12px;
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(0, 242, 255, 0.3);
border-radius: 6px;
color: white;
outline: none;
}
.toolbar input[type="text"]:focus {
border-color: var(--primary-color);
}
button {
background: linear-gradient(90deg, #0051ff 0%, #00f2ff 100%);
color: white;
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.3s;
box-shadow: 0 2px 10px rgba(0, 242, 255, 0.3);
}
button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(0, 242, 255, 0.5);
}
button.btn-success {
background: linear-gradient(90deg, #28a745 0%, #20c997 100%);
box-shadow: 0 2px 10px rgba(32, 201, 151, 0.35);
}
button.btn-danger {
background: linear-gradient(90deg, #cc0000 0%, #ff4d4d 100%);
box-shadow: 0 2px 10px rgba(255, 0, 0, 0.3);
}
button.btn-warning {
background: linear-gradient(90deg, #ffc107 0%, #fd7e14 100%);
box-shadow: 0 2px 10px rgba(255, 193, 7, 0.35);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.table-container {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
th, td {
padding: 10px 12px;
text-align: left;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
th {
background: rgba(0, 0, 0, 0.3);
color: var(--primary-color);
font-weight: 600;
white-space: nowrap;
position: sticky;
top: 0;
}
tr:hover {
background: rgba(0, 242, 255, 0.05);
}
td {
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.action-btns {
display: flex;
gap: 5px;
}
.action-btns button {
padding: 4px 8px;
font-size: 0.8rem;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
margin-top: 15px;
}
.pagination button {
padding: 6px 12px;
}
.pagination span {
color: var(--text-muted);
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal.show {
display: flex;
}
.modal-content {
background: var(--card-bg);
border-radius: 16px;
padding: 25px;
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
border: 1px solid rgba(0, 242, 255, 0.3);
backdrop-filter: blur(20px);
}
.modal h2 {
margin-bottom: 20px;
color: var(--primary-color);
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: var(--text-muted);
font-size: 0.9rem;
}
.form-group input, .form-group textarea, .form-group select {
width: 100%;
padding: 10px;
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(0, 242, 255, 0.3);
border-radius: 6px;
color: white;
font-size: 0.95rem;
}
.form-group textarea {
min-height: 80px;
resize: vertical;
}
.form-group input:focus, .form-group textarea:focus {
outline: none;
border-color: var(--primary-color);
}
.modal-btns {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 20px;
}
#message {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 25px;
border-radius: 8px;
color: white;
font-weight: bold;
display: none;
z-index: 1001;
backdrop-filter: blur(10px);
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
}
.success {
background: rgba(40, 167, 69, 0.9);
border: 1px solid #28a745;
}
.error {
background: rgba(220, 53, 69, 0.9);
border: 1px solid #dc3545;
}
.table-info {
display: flex;
gap: 20px;
margin-bottom: 15px;
color: var(--text-muted);
font-size: 0.9rem;
}
.loading {
text-align: center;
padding: 40px;
color: var(--text-muted);
}
.empty-state {
text-align: center;
padding: 60px;
color: var(--text-muted);
}
.checkbox-cell {
width: 40px;
text-align: center;
}
.checkbox-cell input {
width: 18px;
height: 18px;
accent-color: var(--primary-color);
}
</style>
</head>
<body>
<!-- Password Overlay -->
<div id="password-overlay" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: var(--bg-color); z-index: 9999; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(10px);">
<div class="card" style="text-align: center; width: 90%; max-width: 400px;">
<h2 style="margin-bottom: 20px; color: var(--primary-color);">请输入访问密钥</h2>
<div class="form-group">
<input type="password" id="secret-input" placeholder="输入密钥" style="text-align: center;">
</div>
<button onclick="checkSecret()">确认</button>
<p id="secret-error" style="color: #ff6b6b; margin-top: 15px; display: none;">密钥错误</p>
</div>
</div>
<div class="container">
<h1>数据库管理</h1>
<div class="nav-bar">
<a href="/admin">← 返回管理后台</a>
</div>
<div class="card">
<h2 style="margin-bottom: 15px; color: var(--primary-color);">选择数据表</h2>
<div class="table-list" id="tableList">
<div class="loading">加载中...</div>
</div>
</div>
<div class="card" id="dataCard" style="display: none;">
<h2 style="margin-bottom: 15px; color: var(--primary-color);" id="tableTitle">数据表</h2>
<div class="table-info" id="tableInfo"></div>
<div class="toolbar">
<input type="text" id="searchInput" placeholder="搜索..." onkeyup="handleSearch()">
<button onclick="loadData()">🔄 刷新</button>
<button class="btn-success" onclick="showInsertModal()"> 新增</button>
<button class="btn-danger" id="deleteSelectedBtn" onclick="deleteSelected()" disabled>🗑️ 删除选中</button>
</div>
<div class="table-container" id="tableContainer">
<div class="loading">请先选择数据表</div>
</div>
<div class="pagination" id="pagination"></div>
</div>
</div>
<div class="modal" id="editModal">
<div class="modal-content">
<h2 id="modalTitle">编辑数据</h2>
<form id="editForm">
<div id="formFields"></div>
<div class="modal-btns">
<button type="button" onclick="closeModal()">取消</button>
<button type="submit" class="btn-success" id="submitBtn">保存</button>
</div>
</form>
</div>
</div>
<div id="message"></div>
<script>
document.addEventListener('DOMContentLoaded', checkAuthStatus);
async function checkAuthStatus() {
try {
const res = await fetch('/api/admin/check-auth');
const data = await res.json();
if (data.authenticated) {
hideOverlay();
} else {
showOverlay();
}
} catch (e) {
showOverlay();
}
}
function showOverlay() {
document.getElementById('password-overlay').style.display = 'flex';
}
function hideOverlay() {
const overlay = document.getElementById('password-overlay');
overlay.style.opacity = '0';
setTimeout(() => {
overlay.style.display = 'none';
}, 500);
}
async function checkSecret() {
const input = document.getElementById('secret-input');
const error = document.getElementById('secret-error');
const password = input.value;
try {
const res = await fetch('/api/admin/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({password: password})
});
const data = await res.json();
if (data.success) {
error.style.display = 'none';
hideOverlay();
} else {
error.style.display = 'block';
input.value = '';
input.focus();
}
} catch (e) {
error.textContent = '网络错误';
error.style.display = 'block';
}
}
document.getElementById('secret-input').addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
checkSecret();
}
});
let currentTable = '';
let currentPage = 1;
let pageSize = 50;
let totalRows = 0;
let tableColumns = [];
let tableData = [];
let primaryKey = '';
let editingId = null;
// 加载表列表
async function loadTables() {
try {
const res = await fetch('/api/db/tables');
const data = await res.json();
if (data.success) {
const tableList = document.getElementById('tableList');
tableList.innerHTML = '';
data.tables.forEach(table => {
const btn = document.createElement('button');
btn.className = 'table-btn';
btn.textContent = table;
btn.onclick = () => selectTable(table);
if (currentTable === table) btn.classList.add('active');
tableList.appendChild(btn);
});
}
} catch (e) {
showMessage('加载表列表失败', 'error');
}
}
// 选择表
async function selectTable(table) {
currentTable = table;
currentPage = 1;
document.getElementById('dataCard').style.display = 'block';
document.getElementById('tableTitle').textContent = table + ' - 数据管理';
document.querySelectorAll('.table-btn').forEach(btn => {
btn.classList.remove('active');
if (btn.textContent === table) btn.classList.add('active');
});
await loadData();
}
// 加载数据
async function loadData() {
if (!currentTable) return;
try {
const res = await fetch(`/api/db/table/${currentTable}?page=${currentPage}&page_size=${pageSize}`);
const data = await res.json();
if (data.success) {
tableColumns = data.columns;
tableData = data.rows;
totalRows = data.total;
primaryKey = data.primary_key || '';
renderTable();
renderPagination();
} else {
showMessage(data.message || '加载数据失败', 'error');
}
} catch (e) {
showMessage('网络错误', 'error');
}
}
// 渲染表格
function renderTable() {
const container = document.getElementById('tableContainer');
if (tableData.length === 0) {
container.innerHTML = '<div class="empty-state">暂无数据</div>';
return;
}
let html = '<table><thead><tr>';
// 复选框列
html += '<th class="checkbox-cell"><input type="checkbox" onchange="toggleAllCheckboxes(this)"></th>';
// 数据列
tableColumns.forEach(col => {
html += `<th>${col}</th>`;
});
// 操作列
html += '<th>操作</th></tr></thead><tbody>';
tableData.forEach(row => {
const id = primaryKey ? row[primaryKey] : JSON.stringify(row);
html += `<tr data-id="${id}">`;
html += `<td class="checkbox-cell"><input type="checkbox" class="row-checkbox" data-id="${id}"></td>`;
tableColumns.forEach(col => {
let value = row[col];
if (value === null || value === undefined) value = '';
value = String(value);
if (value.length > 50) {
value = value.substring(0, 50) + '...';
}
html += `<td title="${row[col] || ''}">${escapeHtml(value)}</td>`;
});
html += `<td class="action-btns">
<button class="btn-warning" onclick="showEditModal('${escapeId(id)}')">编辑</button>
<button class="btn-danger" onclick="deleteRow('${escapeId(id)}')">删除</button>
</td></tr>`;
});
html += '</tbody></table>';
container.innerHTML = html;
}
// 转义ID用于HTML属性
function escapeId(id) {
return String(id).replace(/'/g, "\\'").replace(/"/g, '&quot;');
}
// HTML转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 渲染分页
function renderPagination() {
const totalPages = Math.ceil(totalRows / pageSize);
const pagination = document.getElementById('pagination');
if (totalPages <= 1) {
pagination.innerHTML = '';
return;
}
let html = `
<button ${currentPage === 1 ? 'disabled' : ''} onclick="goToPage(1)">首页</button>
<button ${currentPage === 1 ? 'disabled' : ''} onclick="goToPage(${currentPage - 1})">上一页</button>
<span>第 ${currentPage} / ${totalPages} 页 (共 ${totalRows} 条)</span>
<button ${currentPage === totalPages ? 'disabled' : ''} onclick="goToPage(${currentPage + 1})">下一页</button>
<button ${currentPage === totalPages ? 'disabled' : ''} onclick="goToPage(${totalPages})">末页</button>
`;
pagination.innerHTML = html;
}
// 跳转到指定页
function goToPage(page) {
currentPage = page;
loadData();
}
// 搜索
let searchTimeout;
function handleSearch() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
const keyword = document.getElementById('searchInput').value;
filterData(keyword);
}, 300);
}
// 过滤数据(前端过滤)
function filterData(keyword) {
if (!keyword) {
renderTable();
return;
}
keyword = keyword.toLowerCase();
const filtered = tableData.filter(row => {
return tableColumns.some(col => {
const val = String(row[col] || '').toLowerCase();
return val.includes(keyword);
});
});
// 临时显示过滤结果
const originalData = tableData;
tableData = filtered;
renderTable();
tableData = originalData;
}
// 全选/取消全选
function toggleAllCheckboxes(source) {
const checkboxes = document.querySelectorAll('.row-checkbox');
checkboxes.forEach(cb => cb.checked = source.checked);
updateDeleteButton();
}
// 更新删除按钮状态
function updateDeleteButton() {
const checked = document.querySelectorAll('.row-checkbox:checked');
document.getElementById('deleteSelectedBtn').disabled = checked.length === 0;
}
// 监听复选框变化
document.addEventListener('change', (e) => {
if (e.target.classList.contains('row-checkbox')) {
updateDeleteButton();
}
});
// 显示新增弹窗
async function showInsertModal() {
editingId = null;
document.getElementById('modalTitle').textContent = '新增数据 - ' + currentTable;
await renderFormFields();
document.getElementById('editModal').classList.add('show');
}
// 显示编辑弹窗
async function showEditModal(id) {
editingId = id;
document.getElementById('modalTitle').textContent = '编辑数据 - ' + currentTable;
// 查找当前行数据
const row = tableData.find(r => {
const pk = primaryKey;
return pk ? String(r[pk]) === id : JSON.stringify(r) === id;
});
await renderFormFields(row);
document.getElementById('editModal').classList.add('show');
}
// 渲染表单字段
async function renderFormFields(data = {}) {
const formFields = document.getElementById('formFields');
// 获取表结构
try {
const res = await fetch(`/api/db/table/${currentTable}/columns`);
const result = await res.json();
if (!result.success) {
showMessage('获取表结构失败', 'error');
return;
}
let html = '';
result.columns.forEach(col => {
const isPrimary = col.column_name === primaryKey;
if (isPrimary && !data[primaryKey]) {
return; // 新增时跳过自增主键
}
let value = data[col.column_name] || '';
html += `<div class="form-group">
<label>${col.column_name}${col.is_nullable === 'YES' ? '' : ' *'}</label>
<input type="text"
name="${col.column_name}"
value="${escapeHtml(String(value))}"
${isPrimary ? 'readonly' : ''}
data-type="${col.data_type}">
</div>`;
});
formFields.innerHTML = html;
} catch (e) {
showMessage('网络错误', 'error');
}
}
// 关闭弹窗
function closeModal() {
document.getElementById('editModal').classList.remove('show');
editingId = null;
}
// 提交表单
document.getElementById('editForm').onsubmit = async (e) => {
e.preventDefault();
const formData = {};
const inputs = document.querySelectorAll('#editForm input');
inputs.forEach(input => {
formData[input.name] = input.value;
});
const url = editingId
? `/api/db/table/${currentTable}/update`
: `/api/db/table/${currentTable}/insert`;
const method = editingId ? 'PUT' : 'POST';
try {
const res = await fetch(url, {
method: method,
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
id: editingId,
data: formData
})
});
const result = await res.json();
if (result.success) {
showMessage(editingId ? '更新成功' : '添加成功', 'success');
closeModal();
loadData();
} else {
showMessage(result.message || '操作失败', 'error');
}
} catch (e) {
showMessage('网络错误', 'error');
}
};
// 删除单行
async function deleteRow(id) {
if (!confirm('确定要删除这条记录吗?')) return;
try {
const res = await fetch(`/api/db/table/${currentTable}/delete`, {
method: 'DELETE',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ ids: [id] })
});
const result = await res.json();
if (result.success) {
showMessage('删除成功', 'success');
loadData();
} else {
showMessage(result.message || '删除失败', 'error');
}
} catch (e) {
showMessage('网络错误', 'error');
}
}
// 删除选中
async function deleteSelected() {
const checkboxes = document.querySelectorAll('.row-checkbox:checked');
const ids = Array.from(checkboxes).map(cb => cb.dataset.id);
if (ids.length === 0) return;
if (!confirm(`确定要删除选中的 ${ids.length} 条记录吗?`)) return;
try {
const res = await fetch(`/api/db/table/${currentTable}/delete`, {
method: 'DELETE',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ ids: ids })
});
const result = await res.json();
if (result.success) {
showMessage('删除成功', 'success');
loadData();
} else {
showMessage(result.message || '删除失败', 'error');
}
} catch (e) {
showMessage('网络错误', 'error');
}
}
// 显示消息
function showMessage(text, type) {
const msg = document.getElementById('message');
msg.textContent = text;
msg.className = type;
msg.style.display = 'block';
setTimeout(() => msg.style.display = 'none', 3000);
}
// 初始化
loadTables();
</script>
</body>
</html>