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

708 lines
23 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>数据管理</title>
<style>
body{margin:0;font-family: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,.1);z-index:1000;display:flex;flex-direction:column;align-items:center}
.user-id{text-align:center;margin-bottom:0}
.sidebar h3{margin: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:.2s}
.sidebar a:hover,.sidebar button:hover{color:#ff79c6;background-color:rgba(139,233,253,.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,.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:#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,.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: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(0)}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:rgba(0,0,0,.9);overflow:hidden}
.image-modal-content{margin:auto;display:block;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);max-width:80%;max-height:80%;object-fit:contain;border-radius:8px;box-shadow:0 10px 30px rgba(0,0,0,.5);cursor:grab;transition:transform .3s ease}
.image-modal-content.dragging{cursor:grabbing}
.image-modal-close{position:absolute;top:15px;right:35px;color:#f1f1f1;font-size:40px;font-weight:bold;transition:.3s;cursor:pointer;z-index:2001}
.image-modal-close:hover{color:#bbb}
.zoom-controls{position:absolute;bottom:30px;left:50%;transform:translateX(-50%);display:flex;gap:10px;z-index:2001}
.zoom-btn{background:rgba(255,255,255,.7);border:none;border-radius:50%;width:40px;height:40px;font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 10px rgba(0,0,0,.3);transition:background .3s}
.zoom-btn:hover{background:rgba(255,255,255,.9)}
.zoom-info{position:absolute;top:15px;left:15px;color:#f1f1f1;font-size:14px;z-index:2001;background:rgba(0,0,0,.5);padding:5px 10px;border-radius:4px}
</style>
</head>
<body>
<!-- 左侧固定栏目 -->
<div class="sidebar">
<div class="user-id">
<h3>你好,{{ username|default:"访客" }}</h3>
</div>
<div class="navigation-links">
<a href="{% url 'main:home' %}">返回主页</a>
<a id="logoutBtn">退出登录</a>
<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>
<div class="zoom-info">缩放: <span id="zoomValue">100%</span></div>
<img class="image-modal-content" id="expandedImage">
<div class="zoom-controls">
<button class="zoom-btn" id="zoomOutBtn">-</button>
<button class="zoom-btn" id="resetZoomBtn">1:1</button>
<button class="zoom-btn" id="zoomInBtn">+</button>
</div>
</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');
const zoomInBtn = document.getElementById('zoomInBtn');
const zoomOutBtn = document.getElementById('zoomOutBtn');
const resetZoomBtn = document.getElementById('resetZoomBtn');
const zoomValue = document.getElementById('zoomValue');
// 全局变量
let currentId = '';
let currentWriter = '';
let currentImage = '';
let allDataCache = []; // 缓存所有数据,避免重复请求
// 图片缩放相关变量
let currentScale = 1;
let currentX = 0;
let currentY = 0;
let isDragging = false;
let dragStartX = 0;
let dragStartY = 0;
let imgStartX = 0;
let imgStartY = 0;
// 搜索功能
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 || '发生错误';
}
});
// 图片缩放功能
function updateZoom() {
expandedImage.style.transform = `translate(-50%, -50%) scale(${currentScale}) translate(${currentX}px, ${currentY}px)`;
zoomValue.textContent = `${Math.round(currentScale * 100)}%`;
}
function resetZoom() {
currentScale = 1;
currentX = 0;
currentY = 0;
updateZoom();
}
function zoomIn() {
currentScale *= 1.2;
updateZoom();
}
function zoomOut() {
currentScale /= 1.2;
if (currentScale < 0.1) currentScale = 0.1; // 最小缩放限制
updateZoom();
}
// 图片放大功能
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';
// 重置缩放状态
resetZoom();
}
});
// 点击关闭按钮关闭模态框
imageModalClose.onclick = function() {
imageModal.style.display = 'none';
}
// 点击模态框外部区域关闭模态框
window.onclick = function(event) {
if (event.target === imageModal) {
imageModal.style.display = 'none';
}
}
// 缩放按钮事件
zoomInBtn.addEventListener('click', zoomIn);
zoomOutBtn.addEventListener('click', zoomOut);
resetZoomBtn.addEventListener('click', resetZoom);
// 鼠标滚轮缩放
expandedImage.addEventListener('wheel', function(e) {
e.preventDefault();
if (e.deltaY < 0) {
zoomIn();
} else {
zoomOut();
}
});
// 拖拽功能
expandedImage.addEventListener('mousedown', function(e) {
isDragging = true;
dragStartX = e.clientX;
dragStartY = e.clientY;
imgStartX = currentX;
imgStartY = currentY;
expandedImage.classList.add('dragging');
});
document.addEventListener('mousemove', function(e) {
if (isDragging) {
const deltaX = e.clientX - dragStartX;
const deltaY = e.clientY - dragStartY;
currentX = imgStartX + deltaX / currentScale;
currentY = imgStartY + deltaY / currentScale;
updateZoom();
}
});
document.addEventListener('mouseup', function() {
isDragging = false;
expandedImage.classList.remove('dragging');
});
// 触摸事件支持(移动端)
expandedImage.addEventListener('touchstart', function(e) {
if (e.touches.length === 1) {
isDragging = true;
dragStartX = e.touches[0].clientX;
dragStartY = e.touches[0].clientY;
imgStartX = currentX;
imgStartY = currentY;
} else if (e.touches.length === 2) {
// 双指缩放
initialDistance = getDistance(e.touches[0], e.touches[1]);
initialScale = currentScale;
}
});
document.addEventListener('touchmove', function(e) {
e.preventDefault();
if (isDragging && e.touches.length === 1) {
const deltaX = e.touches[0].clientX - dragStartX;
const deltaY = e.touches[0].clientY - dragStartY;
currentX = imgStartX + deltaX / currentScale;
currentY = imgStartY + deltaY / currentScale;
updateZoom();
} else if (e.touches.length === 2) {
// 双指缩放
const currentDistance = getDistance(e.touches[0], e.touches[1]);
const scale = (currentDistance / initialDistance) * initialScale;
currentScale = Math.max(0.1, Math.min(scale, 10)); // 限制缩放范围
updateZoom();
}
});
document.addEventListener('touchend', function() {
isDragging = false;
});
// 计算两点间距离
function getDistance(touch1, touch2) {
return Math.sqrt(
Math.pow(touch2.clientX - touch1.clientX, 2) +
Math.pow(touch2.clientY - touch1.clientY, 2)
);
}
});
</script>
</body>
</html>