Compare commits
2 Commits
ee46e4cebb
...
9dcd353815
| Author | SHA1 | Date | |
|---|---|---|---|
| 9dcd353815 | |||
| 42758deeae |
@@ -4,27 +4,191 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>数据管理</title>
|
<title>数据管理</title>
|
||||||
<style>
|
<style>
|
||||||
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; background:#fafafa; }
|
body {
|
||||||
.container { max-width: 1100px; margin: 6vh auto; background:#fff; border-radius:10px; box-shadow:0 6px 18px rgba(0,0,0,0.06); padding:20px; }
|
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||||
table { width:100%; border-collapse: collapse; }
|
background:#fafafa;
|
||||||
th, td { border-bottom:1px solid #eee; padding:8px; text-align:left; vertical-align: top; }
|
margin: 0;
|
||||||
img { max-width: 120px; border:1px solid #eee; border-radius:6px; }
|
padding: 20px;
|
||||||
.btn { padding:6px 10px; border:none; border-radius:6px; cursor:pointer; }
|
}
|
||||||
.btn-primary { background:#1677ff; color:#fff; }
|
.container {
|
||||||
.btn-danger { background:#ff4d4f; color:#fff; }
|
max-width: 1200px;
|
||||||
.btn-secondary { background:#f0f0f0; }
|
margin: 0 auto;
|
||||||
.muted { color:#666; font-size:12px; }
|
background:#fff;
|
||||||
.modal { position: fixed; inset: 0; display: none; background: rgba(0,0,0,0.4); align-items: center; justify-content: center; }
|
border-radius:10px;
|
||||||
.modal .dialog { width: 720px; max-width: 92vw; background:#fff; border-radius:10px; padding:16px; }
|
box-shadow:0 6px 18px rgba(0,0,0,0.06);
|
||||||
textarea { width:100%; min-height: 240px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:14px; }
|
padding:20px;
|
||||||
#kvForm { border:1px solid #eee; border-radius:6px; padding:8px; max-height:300px; overflow:auto; }
|
}
|
||||||
|
table {
|
||||||
|
width:100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
border-bottom:1px solid #eee;
|
||||||
|
padding:12px 8px;
|
||||||
|
text-align:left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 120px;
|
||||||
|
border:1px solid #eee;
|
||||||
|
border-radius:6px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding:6px 10px;
|
||||||
|
border:none;
|
||||||
|
border-radius:6px;
|
||||||
|
cursor:pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background:#1677ff;
|
||||||
|
color:#fff;
|
||||||
|
}
|
||||||
|
.btn-danger {
|
||||||
|
background:#ff4d4f;
|
||||||
|
color:#fff;
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
background:#f0f0f0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.muted {
|
||||||
|
color:#666;
|
||||||
|
font-size:12px;
|
||||||
|
}
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
display: none;
|
||||||
|
background: rgba(0,0,0,0.4);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.modal .dialog {
|
||||||
|
width: 720px;
|
||||||
|
max-width: 92vw;
|
||||||
|
background:#fff;
|
||||||
|
border-radius:10px;
|
||||||
|
padding:20px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
width:100%;
|
||||||
|
min-height: 240px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
font-size:14px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
#kvForm {
|
||||||
|
border:1px solid #eee;
|
||||||
|
border-radius:6px;
|
||||||
|
padding:8px;
|
||||||
|
max-height:300px;
|
||||||
|
overflow:auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索区域样式 */
|
||||||
|
.search-container {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.search-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.search-result {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #e8f4ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.search-result.empty {
|
||||||
|
background: #fff8e8;
|
||||||
|
}
|
||||||
|
.search-result.error {
|
||||||
|
background: #ffe8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载动画 */
|
||||||
|
.loading {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 3px solid #f3f3f3;
|
||||||
|
border-top: 3px solid #1677ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.search-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.search-input {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>数据管理</h2>
|
<h2>数据管理</h2>
|
||||||
<p class="muted">仅管理员可见。可查看、编辑、删除所有记录。</p>
|
<p class="muted">仅管理员可见。可查看、编辑、删除所有记录。</p>
|
||||||
<table>
|
|
||||||
|
<!-- 搜索功能区域 -->
|
||||||
|
<div class="search-container">
|
||||||
|
<div class="search-controls">
|
||||||
|
<input type="text" id="searchQuery" class="search-input" placeholder="请输入搜索关键词...">
|
||||||
|
<button class="btn btn-primary" onclick="performSearch('exact')">关键词搜索</button>
|
||||||
|
<button class="btn btn-secondary" onclick="performSearch('fuzzy')">模糊搜索</button>
|
||||||
|
<button class="btn" onclick="loadAllData()">显示全部</button>
|
||||||
|
<button class="btn" onclick="clearSearch()">清空结果</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="searchResult" class="search-result" style="display: none;">
|
||||||
|
<div id="searchStatus">正在搜索...</div>
|
||||||
|
<div id="searchCount" style="margin-top: 5px; font-weight: bold;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<table id="dataTable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
@@ -34,32 +198,15 @@
|
|||||||
<th>操作</th>
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody id="tableBody">
|
||||||
{% for it in items %}
|
<!-- 数据将通过JavaScript动态加载 -->
|
||||||
<tr data-id="{{ it.id }}" data-writer="{{ it.writer_id }}" data-image="{{ it.image }}">
|
|
||||||
<td style="max-width:140px; word-break:break-all;">{{ it.id }}</td>
|
|
||||||
<td>
|
|
||||||
{% if it.image %}
|
|
||||||
<img src="/media/{{ it.image }}" onerror="this.src='';" />
|
|
||||||
<div class="muted">/media/{{ it.image }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<pre style="white-space:pre-wrap; word-wrap:break-word;">{{ it.data|safe }}</pre>
|
|
||||||
</td>
|
|
||||||
<td>{{ it.writer_id }}</td>
|
|
||||||
<td>
|
|
||||||
<button class="btn btn-primary" onclick="openEdit('{{ it.id }}')">编辑</button>
|
|
||||||
<button class="btn btn-danger" onclick="doDelete('{{ it.id }}')">删除</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div id="modal" class="modal">
|
<!-- 编辑模态框 -->
|
||||||
|
<div id="editModal" class="modal">
|
||||||
<div class="dialog">
|
<div class="dialog">
|
||||||
<h3>编辑</h3>
|
<h3>编辑数据</h3>
|
||||||
<div style="display:flex; gap:8px; align-items:center; margin-bottom:8px;">
|
<div style="display:flex; gap:8px; align-items:center; margin-bottom:8px;">
|
||||||
<button id="addFieldBtn" class="btn btn-secondary" type="button">添加字段</button>
|
<button id="addFieldBtn" class="btn btn-secondary" type="button">添加字段</button>
|
||||||
<button id="syncFromTextBtn" class="btn btn-secondary" type="button">从文本区刷新表单</button>
|
<button id="syncFromTextBtn" class="btn btn-secondary" type="button">从文本区刷新表单</button>
|
||||||
@@ -67,7 +214,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="kvForm"></div>
|
<div id="kvForm"></div>
|
||||||
<div style="margin-top:8px;">
|
<div style="margin-top:8px;">
|
||||||
<textarea id="resultBox" placeholder="JSON"></textarea>
|
<textarea id="resultBox" placeholder="JSON数据"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top:12px; display:flex; gap:8px;">
|
<div style="margin-top:12px; display:flex; gap:8px;">
|
||||||
<button class="btn btn-primary" onclick="saveEdit()">保存</button>
|
<button class="btn btn-primary" onclick="saveEdit()">保存</button>
|
||||||
@@ -79,34 +226,239 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// 获取CSRF token的函数
|
||||||
function getCookie(name) {
|
function getCookie(name) {
|
||||||
const value = `; ${document.cookie}`;
|
const value = `; ${document.cookie}`;
|
||||||
const parts = value.split(`; ${name}=`);
|
const parts = value.split(`; ${name}=`);
|
||||||
if (parts.length === 2) return parts.pop().split(';').shift();
|
if (parts.length === 2) return parts.pop().split(';').shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
const modal = document.getElementById('modal');
|
// DOM元素引用
|
||||||
|
const searchQueryInput = document.getElementById('searchQuery');
|
||||||
|
const searchResultDiv = document.getElementById('searchResult');
|
||||||
|
const searchStatus = document.getElementById('searchStatus');
|
||||||
|
const searchCount = document.getElementById('searchCount');
|
||||||
|
const tableBody = document.getElementById('tableBody');
|
||||||
|
const editModal = document.getElementById('editModal');
|
||||||
const kvForm = document.getElementById('kvForm');
|
const kvForm = document.getElementById('kvForm');
|
||||||
const resultBox = document.getElementById('resultBox');
|
const resultBox = document.getElementById('resultBox');
|
||||||
const editMsg = document.getElementById('editMsg');
|
const editMsg = document.getElementById('editMsg');
|
||||||
const addFieldBtn = document.getElementById('addFieldBtn');
|
const addFieldBtn = document.getElementById('addFieldBtn');
|
||||||
const syncFromTextBtn = document.getElementById('syncFromTextBtn');
|
const syncFromTextBtn = document.getElementById('syncFromTextBtn');
|
||||||
|
|
||||||
|
// 全局变量
|
||||||
let currentId = '';
|
let currentId = '';
|
||||||
let currentWriter = '';
|
let currentWriter = '';
|
||||||
let currentImage = '';
|
let currentImage = '';
|
||||||
|
let allDataCache = []; // 缓存所有数据,避免重复请求
|
||||||
|
|
||||||
|
// 搜索功能
|
||||||
|
async function performSearch(type) {
|
||||||
|
const query = searchQueryInput.value.trim();
|
||||||
|
if (!query) {
|
||||||
|
showSearchMessage('请输入搜索关键词', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showSearchLoading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
let url;
|
||||||
|
if (type === 'exact') {
|
||||||
|
url = `/elastic/search/?q=${encodeURIComponent(query)}`;
|
||||||
|
} else if (type === 'fuzzy') {
|
||||||
|
url = `/elastic/fuzzy-search/?keyword=${encodeURIComponent(query)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.status === 'success') {
|
||||||
|
displaySearchResults(data.data || []);
|
||||||
|
} else {
|
||||||
|
showSearchMessage(`搜索失败: ${data.message || '未知错误'}`, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('搜索请求失败:', error);
|
||||||
|
showSearchMessage('搜索请求失败,请检查网络连接', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示加载状态
|
||||||
|
function showSearchLoading() {
|
||||||
|
searchResultDiv.style.display = 'block';
|
||||||
|
searchResultDiv.className = 'search-result';
|
||||||
|
searchStatus.innerHTML = '<span class="loading"></span> 正在搜索...';
|
||||||
|
searchCount.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示搜索结果
|
||||||
|
function displaySearchResults(results) {
|
||||||
|
if (results.length === 0) {
|
||||||
|
showSearchMessage('未找到匹配的结果', 'empty');
|
||||||
|
renderTable([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchResultDiv.style.display = 'block';
|
||||||
|
searchResultDiv.className = 'search-result';
|
||||||
|
searchStatus.textContent = `找到 ${results.length} 条匹配结果`;
|
||||||
|
searchCount.textContent = `显示 ${results.length} 条记录`;
|
||||||
|
|
||||||
|
renderTable(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示搜索消息
|
||||||
|
function showSearchMessage(message, type = '') {
|
||||||
|
searchResultDiv.style.display = 'block';
|
||||||
|
searchResultDiv.className = `search-result ${type}`;
|
||||||
|
searchStatus.textContent = message;
|
||||||
|
searchCount.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载所有数据
|
||||||
|
async function loadAllData() {
|
||||||
|
showSearchLoading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 如果已有缓存,直接使用
|
||||||
|
if (allDataCache.length > 0) {
|
||||||
|
displayAllData(allDataCache);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('/elastic/all-data/');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.status === 'success') {
|
||||||
|
allDataCache = data.data || [];
|
||||||
|
displayAllData(allDataCache);
|
||||||
|
} else {
|
||||||
|
showSearchMessage(`加载数据失败: ${data.message || '未知错误'}`, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载数据失败:', error);
|
||||||
|
showSearchMessage('加载数据失败,请检查网络连接', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示所有数据
|
||||||
|
function displayAllData(data) {
|
||||||
|
searchResultDiv.style.display = 'block';
|
||||||
|
searchResultDiv.className = 'search-result';
|
||||||
|
searchStatus.textContent = '显示全部数据';
|
||||||
|
searchCount.textContent = `共 ${data.length} 条记录`;
|
||||||
|
|
||||||
|
renderTable(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空搜索结果
|
||||||
|
function clearSearch() {
|
||||||
|
searchQueryInput.value = '';
|
||||||
|
searchResultDiv.style.display = 'none';
|
||||||
|
|
||||||
|
// 如果有缓存数据,显示全部
|
||||||
|
if (allDataCache.length > 0) {
|
||||||
|
renderTable(allDataCache);
|
||||||
|
} else {
|
||||||
|
// 否则重新加载
|
||||||
|
loadAllData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染表格
|
||||||
|
function renderTable(data) {
|
||||||
|
tableBody.innerHTML = '';
|
||||||
|
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.innerHTML = '<td colspan="5" style="text-align: center; color: #999;">暂无数据</td>';
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.forEach(item => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.setAttribute('data-id', item._id || item.id);
|
||||||
|
row.setAttribute('data-writer', item.writer_id);
|
||||||
|
row.setAttribute('data-image', item.image);
|
||||||
|
|
||||||
|
// 解析data字段,如果是JSON字符串则格式化显示
|
||||||
|
let displayData = item.data || '';
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(item.data);
|
||||||
|
displayData = JSON.stringify(parsed, null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
// 如果不是JSON,直接显示原字符串
|
||||||
|
}
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td style="max-width:140px; word-break:break-all; font-size: 12px;">${item._id || item.id || ''}</td>
|
||||||
|
<td>
|
||||||
|
${item.image ? `<img src="/media/${item.image}" onerror="this.src=''; this.alt='图片加载失败'" />` : '无图片'}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<pre style="white-space:pre-wrap; word-wrap:break-word; max-height: 100px; overflow-y: auto; font-size: 12px; margin: 0;">${escapeHtml(displayData)}</pre>
|
||||||
|
</td>
|
||||||
|
<td style="font-size: 12px;">${item.writer_id || ''}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-primary" onclick="openEdit('${item._id || item.id}')">编辑</button>
|
||||||
|
<button class="btn btn-danger" onclick="doDelete('${item._id || item.id}')">删除</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转义HTML以防止XSS
|
||||||
|
function escapeHtml(unsafe) {
|
||||||
|
return unsafe
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑功能相关
|
||||||
function createRow(k = '', v = '') {
|
function createRow(k = '', v = '') {
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.style.display = 'grid';
|
row.style.display = 'grid';
|
||||||
row.style.gridTemplateColumns = '1fr 1fr auto';
|
row.style.gridTemplateColumns = '1fr 1fr auto';
|
||||||
row.style.gap = '8px';
|
row.style.gap = '8px';
|
||||||
row.style.marginBottom = '6px';
|
row.style.marginBottom = '6px';
|
||||||
const kI = document.createElement('input'); kI.type='text'; kI.placeholder='字段名'; kI.value=k;
|
|
||||||
const vI = document.createElement('input'); vI.type='text'; vI.placeholder='字段值'; vI.value = typeof v==='object'? JSON.stringify(v): (v??'');
|
const kI = document.createElement('input');
|
||||||
const del = document.createElement('button'); del.type='button'; del.className='btn'; del.textContent='删除'; del.onclick=()=>{ kvForm.removeChild(row); syncTextarea(); };
|
kI.type='text';
|
||||||
kI.oninput = syncTextarea; vI.oninput = syncTextarea;
|
kI.placeholder='字段名';
|
||||||
row.appendChild(kI); row.appendChild(vI); row.appendChild(del);
|
kI.value=k;
|
||||||
|
|
||||||
|
const vI = document.createElement('input');
|
||||||
|
vI.type='text';
|
||||||
|
vI.placeholder='字段值';
|
||||||
|
vI.value = typeof v==='object'? JSON.stringify(v): (v??'');
|
||||||
|
|
||||||
|
const del = document.createElement('button');
|
||||||
|
del.type='button';
|
||||||
|
del.className='btn';
|
||||||
|
del.textContent='删除';
|
||||||
|
del.onclick=()=>{
|
||||||
|
if (kvForm.children.length > 1) { // 至少保留一行
|
||||||
|
kvForm.removeChild(row);
|
||||||
|
syncTextarea();
|
||||||
|
} else {
|
||||||
|
kI.value = '';
|
||||||
|
vI.value = '';
|
||||||
|
syncTextarea();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
kI.oninput = syncTextarea;
|
||||||
|
vI.oninput = syncTextarea;
|
||||||
|
|
||||||
|
row.appendChild(kI);
|
||||||
|
row.appendChild(vI);
|
||||||
|
row.appendChild(del);
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,61 +468,155 @@ function renderForm(obj){
|
|||||||
if (!kvForm.children.length) kvForm.appendChild(createRow());
|
if (!kvForm.children.length) kvForm.appendChild(createRow());
|
||||||
syncTextarea();
|
syncTextarea();
|
||||||
}
|
}
|
||||||
|
|
||||||
function formToObject(){
|
function formToObject(){
|
||||||
const o={};
|
const o={};
|
||||||
Array.from(kvForm.children).forEach(row=>{
|
Array.from(kvForm.children).forEach(row=>{
|
||||||
const [kI,vI] = row.querySelectorAll('input');
|
const [kI,vI] = row.querySelectorAll('input');
|
||||||
const k=(kI.value||'').trim(); if(!k) return;
|
const k=(kI.value||'').trim(); if(!k) return;
|
||||||
const raw=vI.value; try{ o[k]=JSON.parse(raw);}catch(_){ o[k]=raw; }
|
const raw=vI.value;
|
||||||
|
try{
|
||||||
|
o[k]=JSON.parse(raw);
|
||||||
|
}catch(_){
|
||||||
|
o[k]=raw;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
function syncTextarea(){ resultBox.value = JSON.stringify(formToObject(), null, 2); }
|
|
||||||
|
|
||||||
addFieldBtn.onclick = ()=>{ kvForm.appendChild(createRow()); syncTextarea(); };
|
function syncTextarea(){
|
||||||
syncFromTextBtn.onclick = ()=>{ try{ renderForm(JSON.parse(resultBox.value||'{}')); }catch(e){ editMsg.textContent='JSON无效'; } };
|
try {
|
||||||
|
resultBox.value = JSON.stringify(formToObject(), null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
resultBox.value = '{}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 事件绑定
|
||||||
|
addFieldBtn.onclick = ()=>{
|
||||||
|
kvForm.appendChild(createRow());
|
||||||
|
syncTextarea();
|
||||||
|
};
|
||||||
|
|
||||||
|
syncFromTextBtn.onclick = ()=>{
|
||||||
|
try{
|
||||||
|
const obj = JSON.parse(resultBox.value||'{}');
|
||||||
|
renderForm(obj);
|
||||||
|
editMsg.textContent = '已从文本区刷新表单';
|
||||||
|
setTimeout(() => editMsg.textContent = '', 2000);
|
||||||
|
}catch(e){
|
||||||
|
editMsg.textContent = 'JSON格式无效';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function openEdit(id){
|
function openEdit(id){
|
||||||
const tr = document.querySelector(`tr[data-id="${id}"]`);
|
const tr = document.querySelector(`tr[data-id="${id}"]`);
|
||||||
currentId = id;
|
currentId = id;
|
||||||
currentWriter = tr?.getAttribute('data-writer') || '';
|
currentWriter = tr?.getAttribute('data-writer') || '';
|
||||||
currentImage = tr?.getAttribute('data-image') || '';
|
currentImage = tr?.getAttribute('data-image') || '';
|
||||||
|
|
||||||
fetch(`/elastic/data/${id}/`, { credentials:'same-origin' })
|
fetch(`/elastic/data/${id}/`, { credentials:'same-origin' })
|
||||||
.then(r=>r.json()).then(d=>{
|
.then(r=>r.json()).then(d=>{
|
||||||
if(d.status!=='success') throw new Error('获取失败');
|
if(d.status!=='success') throw new Error(d.message || '获取失败');
|
||||||
const rec=d.data||{};
|
const rec=d.data||{};
|
||||||
const dataStr = rec.data || '{}';
|
const dataStr = rec.data || '{}';
|
||||||
let obj={}; try{ obj = typeof dataStr==='string'? JSON.parse(dataStr): (dataStr||{});}catch(_){ obj={}; }
|
let obj={};
|
||||||
renderForm(obj);
|
try{
|
||||||
modal.style.display='flex';
|
obj = typeof dataStr==='string'? JSON.parse(dataStr): (dataStr||{});
|
||||||
}).catch(e=>{ alert(e.message||'发生错误'); });
|
}catch(_){
|
||||||
|
obj={};
|
||||||
}
|
}
|
||||||
function closeModal(){ modal.style.display='none'; currentId=''; }
|
renderForm(obj);
|
||||||
function saveEdit(){
|
editModal.style.display='flex';
|
||||||
|
}).catch(e=>{
|
||||||
|
alert(e.message||'获取数据失败');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal(){
|
||||||
|
editModal.style.display='none';
|
||||||
|
currentId='';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveEdit(){
|
||||||
const body = {
|
const body = {
|
||||||
writer_id: currentWriter,
|
writer_id: currentWriter,
|
||||||
data: JSON.stringify(formToObject()),
|
data: resultBox.value, // 直接使用textarea中的值
|
||||||
image: currentImage,
|
image: currentImage,
|
||||||
};
|
};
|
||||||
fetch(`/elastic/data/${currentId}/update/`, {
|
|
||||||
method:'PUT', credentials:'same-origin',
|
try {
|
||||||
headers:{ 'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')||'' },
|
const response = await fetch(`/elastic/data/${currentId}/update/`, {
|
||||||
|
method:'PUT',
|
||||||
|
credentials:'same-origin',
|
||||||
|
headers:{
|
||||||
|
'Content-Type':'application/json',
|
||||||
|
'X-CSRFToken': getCookie('csrftoken')||''
|
||||||
|
},
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
}).then(r=>r.json()).then(d=>{
|
});
|
||||||
if(d.status!=='success') throw new Error('保存失败');
|
|
||||||
location.reload();
|
const data = await response.json();
|
||||||
}).catch(e=>{ editMsg.textContent = e.message||'发生错误'; });
|
if(data.status!=='success') throw new Error(data.message || '保存失败');
|
||||||
|
|
||||||
|
alert('保存成功');
|
||||||
|
closeModal();
|
||||||
|
// 重新加载数据以显示更新
|
||||||
|
if (searchResultDiv.style.display !== 'none') {
|
||||||
|
// 如果当前显示的是搜索结果,重新执行搜索
|
||||||
|
const query = searchQueryInput.value.trim();
|
||||||
|
if (query) {
|
||||||
|
const isFuzzy = document.querySelector('.search-result').textContent.includes('模糊');
|
||||||
|
performSearch(isFuzzy ? 'fuzzy' : 'exact');
|
||||||
|
} else {
|
||||||
|
loadAllData();
|
||||||
}
|
}
|
||||||
function doDelete(id){
|
} else {
|
||||||
if(!confirm('确认删除该记录?')) return;
|
loadAllData();
|
||||||
fetch(`/elastic/data/${id}/delete/`, {
|
}
|
||||||
method:'DELETE', credentials:'same-origin',
|
} catch (e) {
|
||||||
|
editMsg.textContent = e.message||'保存失败';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doDelete(id){
|
||||||
|
if(!confirm('确认删除该记录?此操作不可撤销')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/elastic/data/${id}/delete/`, {
|
||||||
|
method:'DELETE',
|
||||||
|
credentials:'same-origin',
|
||||||
headers:{ 'X-CSRFToken': getCookie('csrftoken')||'' }
|
headers:{ 'X-CSRFToken': getCookie('csrftoken')||'' }
|
||||||
}).then(r=>r.json()).then(d=>{
|
});
|
||||||
if(d.status!=='success') throw new Error('删除失败');
|
|
||||||
location.reload();
|
const data = await response.json();
|
||||||
}).catch(e=> alert(e.message||'发生错误'));
|
if(data.status!=='success') throw new Error(data.message || '删除失败');
|
||||||
|
|
||||||
|
alert('删除成功');
|
||||||
|
// 重新加载数据
|
||||||
|
if (searchResultDiv.style.display !== 'none') {
|
||||||
|
const query = searchQueryInput.value.trim();
|
||||||
|
if (query) {
|
||||||
|
const isFuzzy = document.querySelector('.search-result').textContent.includes('模糊');
|
||||||
|
performSearch(isFuzzy ? 'fuzzy' : 'exact');
|
||||||
|
} else {
|
||||||
|
loadAllData();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
loadAllData();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert(e.message||'删除失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时自动加载所有数据
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
loadAllData();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,109 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<title>主页</title>
|
<title>固定左侧栏目</title>
|
||||||
<style>
|
<style>
|
||||||
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; background: #fafafa; }
|
.sidebar {
|
||||||
.container { max-width: 720px; margin: 8vh auto; padding: 24px; }
|
position: fixed;
|
||||||
.card { background: #fff; border-radius: 10px; box-shadow: 0 6px 18px rgba(0,0,0,0.06); padding: 24px; }
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 180px; /* 导航栏变窄 */
|
||||||
|
height: 100vh;
|
||||||
|
background: #1e1e2e;
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 2px 0 5px rgba(0,0,0,0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-id {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: auto; /* 用户ID保持在顶部 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #ff79c6;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px; /* 调整标题与下方内容的距离 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-links {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 60px; /* 空出一个正方形位置(约 60x60px) */
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar a,
|
||||||
|
.sidebar button {
|
||||||
|
display: block;
|
||||||
|
color: #8be9fd;
|
||||||
|
text-decoration: none;
|
||||||
|
margin: 10px 0; /* 减少上下间距 */
|
||||||
|
font-size: 16px; /* 根据需要调整字体大小 */
|
||||||
|
padding: 15px; /* 增加内边距以填充整个宽度 */
|
||||||
|
border-radius: 4px; /* 减少圆角半径 */
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
width: calc(100% - 40px); /* 适应padding-left/right 20px */
|
||||||
|
text-align: left;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar a:hover,
|
||||||
|
.sidebar button:hover {
|
||||||
|
color: #ff79c6;
|
||||||
|
background-color: rgba(139, 233, 253, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: 200px; /* 调整主内容区左外边距,与新侧边栏宽度匹配 */
|
||||||
|
padding: 20px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#logoutMsg {
|
||||||
|
color: #ff5555;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% csrf_token %}
|
|
||||||
<!-- CSRF token to assist logout POST via cookie/header -->
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container" style="display:flex; gap:16px;">
|
<!-- 左侧固定栏目 -->
|
||||||
<aside style="width:220px; background:#fff; border-radius:10px; box-shadow: 0 6px 18px rgba(0,0,0,0.06); padding:16px; height: fit-content;">
|
<div class="sidebar">
|
||||||
<h3 style="margin-top:0; font-size:16px;">导航</h3>
|
<div class="user-id">
|
||||||
<nav>
|
<h3>用户ID:{{ user_id }}</h3>
|
||||||
<ul style="list-style:none; padding-left:0; line-height:1.9;">
|
</div>
|
||||||
<li><a href="/" style="text-decoration:none; color:#1677ff;">主页</a></li>
|
<div class="navigation-links">
|
||||||
<li><a href="/elastic/upload-page/" style="text-decoration:none; color:#1677ff;">图片上传与识别</a></li>
|
<a href="/">主页</a>
|
||||||
<li><a href="/elastic/manage/" style="text-decoration:none; color:#1677ff;">数据管理(管理员)</a></li>
|
<a href="{% url 'elastic:upload_page' %}">图片上传与识别</a>
|
||||||
</ul>
|
<a href="{% url 'elastic:manage_page' %}">数据管理(管理员)</a>
|
||||||
</nav>
|
<button id="logoutBtn">退出登录</button>
|
||||||
<hr/>
|
<div id="logoutMsg"></div>
|
||||||
<button id="logoutBtn" style="padding:8px 12px; width:100%; background:#ff4d4f; color:#fff; border:none; border-radius:6px; cursor:pointer;">退出登录</button>
|
{% csrf_token %}
|
||||||
<div id="logoutMsg" class="muted" style="margin-top:8px;"></div>
|
</div>
|
||||||
</aside>
|
|
||||||
|
|
||||||
<div class="card" style="flex:1;">
|
|
||||||
<h2>主页(留白)</h2>
|
|
||||||
<p>用户ID:{{ user_id }}</p>
|
|
||||||
<p>这里留白即可,主页不由当前实现负责。</p>
|
|
||||||
<p class="muted">提示:已使用安全的会话 Cookie 管理登录状态。</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<div class="main-content">
|
||||||
|
<h1>欢迎来到系统</h1>
|
||||||
|
<p>这里是一大堆内容……</p>
|
||||||
|
<p style="height: 200vh;">滚动试试看,左边菜单不会消失哦!✨</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 登出脚本(保持不变) -->
|
||||||
<script>
|
<script>
|
||||||
function getCookie(name) {
|
function getCookie(name) {
|
||||||
const value = `; ${document.cookie}`;
|
const value = `; ${document.cookie}`;
|
||||||
@@ -66,7 +136,5 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user