Files
Achievement_Inputing/elastic/templates/elastic/manage.html

804 lines
21 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" />
<title>数据管理</title>
<style>
body {
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
background: #fafafa;
}
/* 导航栏样式 */
.sidebar {
position: fixed;
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: 0px;
}
.sidebar h3 {
margin-top: 0;
font-size: 18px;
color: #add8e6;
text-align: center;
margin-bottom: 20px;
}
.navigation-links {
width: 100%;
margin-top: 60px;
}
.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);
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;
}
/* 原有样式保持不变 */
.container {
max-width: 1200px;
margin: 0 auto;
background: #fff;
border-radius: 10px;
box-shadow: 0 6px 18px rgba(0,0,0,0.06);
padding: 20px;
}
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;
cursor: pointer; /* 添加指针样式 */
}
.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;
}
}
/* 图片放大模态框 */
.image-modal {
display: none;
position: fixed;
z-index: 2000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.9);
overflow: auto;
}
.image-modal-content {
margin: auto;
display: block;
width: 80%;
max-width: 800px;
max-height: 80%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.image-modal-close {
position: absolute;
top: 15px;
right: 35px;
color: #f1f1f1;
font-size: 40px;
font-weight: bold;
transition: 0.3s;
cursor: pointer;
z-index: 2001;
}
.image-modal-close:hover {
color: #bbb;
}
</style>
</head>
<body>
<!-- 左侧固定栏目 -->
<div class="sidebar">
<div class="user-id">
<h3>用户ID{{ user_id }}</h3>
</div>
<div class="navigation-links">
<a href="{% url 'main:home' %}">主页</a>
<button id="logoutBtn">退出登录</button>
<div id="logoutMsg"></div>
{% csrf_token %}
</div>
</div>
<!-- 主内容区域 -->
<div class="main-content">
<div class="container">
<h2>数据管理</h2>
<p class="muted">仅管理员可见。可查看、编辑、删除所有记录。</p>
<!-- 搜索功能区域 -->
<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>
<tr>
<th>ID</th>
<th>图片</th>
<th>数据</th>
<th>作者</th>
<th>操作</th>
</tr>
</thead>
<tbody id="tableBody">
<!-- 数据将通过JavaScript动态加载 -->
</tbody>
</table>
<!-- 编辑模态框 -->
<div id="editModal" class="modal">
<div class="dialog">
<h3>编辑数据</h3>
<div style="display:flex; gap:8px; align-items:center; margin-bottom:8px;">
<button id="addFieldBtn" class="btn btn-secondary" type="button">添加字段</button>
<button id="syncFromTextBtn" class="btn btn-secondary" type="button">从文本区刷新表单</button>
<span id="editMsg" class="muted"></span>
</div>
<div id="kvForm"></div>
<div style="margin-top:8px;">
<textarea id="resultBox" placeholder="JSON数据"></textarea>
</div>
<div style="margin-top:12px; display:flex; gap:8px;">
<button class="btn btn-primary" onclick="saveEdit()">保存</button>
<button class="btn" onclick="closeModal()">取消</button>
</div>
</div>
</div>
</div>
</div>
<!-- 图片放大模态框 -->
<div id="imageModal" class="image-modal">
<span class="image-modal-close">&times;</span>
<img class="image-modal-content" id="expandedImage">
</div>
<script>
// 获取CSRF token的函数
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
// 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 resultBox = document.getElementById('resultBox');
const editMsg = document.getElementById('editMsg');
const addFieldBtn = document.getElementById('addFieldBtn');
const syncFromTextBtn = document.getElementById('syncFromTextBtn');
// 图片放大相关元素
const imageModal = document.getElementById('imageModal');
const expandedImage = document.getElementById('expandedImage');
const imageModalClose = document.querySelector('.image-modal-close');
// 全局变量
let currentId = '';
let currentWriter = '';
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='图片加载失败'" class="clickable-image" data-image="/media/${item.image}" />` : '无图片'}
</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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// 编辑功能相关
function createRow(k = '', v = '') {
const row = document.createElement('div');
row.style.display = 'grid';
row.style.gridTemplateColumns = '1fr 1fr auto';
row.style.gap = '8px';
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 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;
}
function renderForm(obj){
kvForm.innerHTML='';
Object.keys(obj||{}).forEach(k=> kvForm.appendChild(createRow(k, obj[k])));
if (!kvForm.children.length) kvForm.appendChild(createRow());
syncTextarea();
}
function formToObject(){
const o={};
Array.from(kvForm.children).forEach(row=>{
const [kI,vI] = row.querySelectorAll('input');
const k=(kI.value||'').trim(); if(!k) return;
const raw=vI.value;
try{
o[k]=JSON.parse(raw);
}catch(_){
o[k]=raw;
}
});
return o;
}
function syncTextarea(){
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){
const tr = document.querySelector(`tr[data-id="${id}"]`);
currentId = id;
currentWriter = tr?.getAttribute('data-writer') || '';
currentImage = tr?.getAttribute('data-image') || '';
fetch(`/elastic/data/${id}/`, { credentials:'same-origin' })
.then(r=>r.json()).then(d=>{
if(d.status!=='success') throw new Error(d.message || '获取失败');
const rec=d.data||{};
const dataStr = rec.data || '{}';
let obj={};
try{
obj = typeof dataStr==='string'? JSON.parse(dataStr): (dataStr||{});
}catch(_){
obj={};
}
renderForm(obj);
editModal.style.display='flex';
}).catch(e=>{
alert(e.message||'获取数据失败');
});
}
function closeModal(){
editModal.style.display='none';
currentId='';
}
async function saveEdit(){
const body = {
writer_id: currentWriter,
data: resultBox.value, // 直接使用textarea中的值
image: currentImage,
};
try {
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),
});
const data = await response.json();
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();
}
} else {
loadAllData();
}
} 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')||'' }
});
const data = await response.json();
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();
});
// 退出登录处理
document.getElementById('logoutBtn').addEventListener('click', async () => {
const msg = document.getElementById('logoutMsg');
msg.textContent = '';
const csrftoken = getCookie('csrftoken');
try {
const resp = await fetch('/accounts/logout/', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrftoken || ''
},
body: JSON.stringify({})
});
const data = await resp.json();
if (!resp.ok || !data.ok) {
throw new Error('登出失败');
}
document.cookie = 'sessionid=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
document.cookie = 'csrftoken=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
window.location.href = data.redirect_url;
} catch (e) {
msg.textContent = e.message || '发生错误';
}
});
// 图片放大功能
document.addEventListener('DOMContentLoaded', function() {
// 为所有图片添加点击事件监听器
document.addEventListener('click', function(e) {
if (e.target.classList.contains('clickable-image')) {
const imgSrc = e.target.src;
expandedImage.src = imgSrc;
imageModal.style.display = 'block';
}
});
// 点击关闭按钮关闭模态框
imageModalClose.onclick = function() {
imageModal.style.display = 'none';
}
// 点击模态框外部区域关闭模态框
window.onclick = function(event) {
if (event.target === imageModal) {
imageModal.style.display = 'none';
}
}
});
</script>
</body>
</html>