Merge remote-tracking branch 'origin/Django' into Django

# Conflicts:
#	main/templates/main/home.html
This commit is contained in:
2025-11-15 09:39:24 +08:00
6 changed files with 687 additions and 270 deletions

Binary file not shown.

View File

@@ -232,7 +232,7 @@ def update_by_id(doc_id, updated_data):
try:
# 获取文档
achievement = AchievementDocument.get(id=doc_id)
print(doc_id)
# 更新字段
if 'writer_id' in updated_data:
achievement.writer_id = updated_data['writer_id']
@@ -391,6 +391,26 @@ def write_user_data(user_data):
print(f"用户数据写入失败: {str(e)}")
return False
def get_user_by_id(user_id):
try:
search = UserDocument.search()
search = search.query("term", user_id=user_id)
response = search.execute()
if response.hits:
hit = response.hits[0]
return {
"user_id": hit.user_id,
"username": hit.username,
"password": hit.password,
"permission": hit.permission
}
return None
except Exception as e:
print(f"获取用户数据失败: {str(e)}")
return None
def get_user_by_username(username):
"""
根据用户名获取用户数据

View File

@@ -1,5 +1,5 @@
INDEX_NAME = "wordsearch266666789"
USER_NAME = "users_123"
INDEX_NAME = "wordsearch266666"
USER_NAME = "users"
ACHIEVEMENT_INDEX_NAME = INDEX_NAME
USER_INDEX_NAME = USER_NAME
GLOBAL_INDEX_NAME = "global11111"

View File

@@ -4,27 +4,191 @@
<meta charset="UTF-8" />
<title>数据管理</title>
<style>
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; background:#fafafa; }
.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; }
table { width:100%; border-collapse: collapse; }
th, td { border-bottom:1px solid #eee; padding:8px; text-align:left; vertical-align: top; }
img { max-width: 120px; border:1px solid #eee; border-radius:6px; }
.btn { padding:6px 10px; border:none; border-radius:6px; cursor:pointer; }
.btn-primary { background:#1677ff; color:#fff; }
.btn-danger { background:#ff4d4f; color:#fff; }
.btn-secondary { background:#f0f0f0; }
.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; }
.modal .dialog { width: 720px; max-width: 92vw; background:#fff; border-radius:10px; padding:16px; }
textarea { width:100%; min-height: 240px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:14px; }
#kvForm { border:1px solid #eee; border-radius:6px; padding:8px; max-height:300px; overflow:auto; }
body {
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
background:#fafafa;
margin: 0;
padding: 20px;
}
.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;
}
.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>
</head>
<body>
<div class="container">
<h2>数据管理</h2>
<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>
<tr>
<th>ID</th>
@@ -34,32 +198,15 @@
<th>操作</th>
</tr>
</thead>
<tbody>
{% for it in items %}
<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 id="tableBody">
<!-- 数据将通过JavaScript动态加载 -->
</tbody>
</table>
<div id="modal" class="modal">
<!-- 编辑模态框 -->
<div id="editModal" class="modal">
<div class="dialog">
<h3>编辑</h3>
<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>
@@ -67,7 +214,7 @@
</div>
<div id="kvForm"></div>
<div style="margin-top:8px;">
<textarea id="resultBox" placeholder="JSON"></textarea>
<textarea id="resultBox" placeholder="JSON数据"></textarea>
</div>
<div style="margin-top:12px; display:flex; gap:8px;">
<button class="btn btn-primary" onclick="saveEdit()">保存</button>
@@ -79,34 +226,239 @@
</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();
}
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 resultBox = document.getElementById('resultBox');
const editMsg = document.getElementById('editMsg');
const addFieldBtn = document.getElementById('addFieldBtn');
const syncFromTextBtn = document.getElementById('syncFromTextBtn');
// 全局变量
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='图片加载失败'" />` : '无图片'}
</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=()=>{ kvForm.removeChild(row); syncTextarea(); };
kI.oninput = syncTextarea; vI.oninput = syncTextarea;
row.appendChild(kI); row.appendChild(vI); row.appendChild(del);
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;
}
@@ -116,61 +468,155 @@ function renderForm(obj){
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; }
const raw=vI.value;
try{
o[k]=JSON.parse(raw);
}catch(_){
o[k]=raw;
}
});
return o;
}
function syncTextarea(){ resultBox.value = JSON.stringify(formToObject(), null, 2); }
addFieldBtn.onclick = ()=>{ kvForm.appendChild(createRow()); syncTextarea(); };
syncFromTextBtn.onclick = ()=>{ try{ renderForm(JSON.parse(resultBox.value||'{}')); }catch(e){ editMsg.textContent='JSON无效'; } };
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('获取失败');
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={}; }
let obj={};
try{
obj = typeof dataStr==='string'? JSON.parse(dataStr): (dataStr||{});
}catch(_){
obj={};
}
renderForm(obj);
modal.style.display='flex';
}).catch(e=>{ alert(e.message||'发生错误'); });
editModal.style.display='flex';
}).catch(e=>{
alert(e.message||'获取数据失败');
});
}
function closeModal(){ modal.style.display='none'; currentId=''; }
function saveEdit(){
function closeModal(){
editModal.style.display='none';
currentId='';
}
async function saveEdit(){
const body = {
writer_id: currentWriter,
data: JSON.stringify(formToObject()),
data: resultBox.value, // 直接使用textarea中的值
image: currentImage,
};
fetch(`/elastic/data/${currentId}/update/`, {
method:'PUT', credentials:'same-origin',
headers:{ 'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')||'' },
body: JSON.stringify(body),
}).then(r=>r.json()).then(d=>{
if(d.status!=='success') throw new Error('保存失败');
location.reload();
}).catch(e=>{ editMsg.textContent = e.message||'发生错误'; });
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||'保存失败';
}
}
function doDelete(id){
if(!confirm('确认删除该记录?')) return;
fetch(`/elastic/data/${id}/delete/`, {
method:'DELETE', credentials:'same-origin',
headers:{ 'X-CSRFToken': getCookie('csrftoken')||'' }
}).then(r=>r.json()).then(d=>{
if(d.status!=='success') throw new Error('删除失败');
location.reload();
}).catch(e=> alert(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();
});
</script>
</body>
</html>

View File

@@ -93,21 +93,32 @@ def analytics_overview(request):
@csrf_exempt
def delete_data(request, doc_id):
"""删除数据(需登录;管理员或作者本人)"""
if not request.session.get("user_id"):
request_user=request.session.get("user_id")
# request_admin=request.session.get("permisssion")
if request_user is None:
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
try:
existing = get_by_id(doc_id)
user_existing=get_user_by_id(request_user)
if not existing:
return JsonResponse({"status": "error", "message": "数据不存在"}, status=404)
is_admin = (request.session.get("permission", 1) == 0)
is_admin = (user_existing.get('permission') ) == 0
is_owner = str(existing.get("writer_id", "")) == str(request.session.get("user_id"))
if not (is_admin or is_owner):
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
success = delete_by_id(doc_id)
if success:
return JsonResponse({"status": "success", "message": "数据删除成功"})
else:
return JsonResponse({"status": "error", "message": "数据删除失败"}, status=500)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@@ -116,18 +127,24 @@ def delete_data(request, doc_id):
@csrf_exempt
def update_data(request, doc_id):
"""更新数据(需登录;管理员或作者本人)"""
# if not request.session.get("user_id"):
# return JsonResponse({"status": "error", "message": "未登录"}, status=401)
request_user = request.session.get("user_id")
if request_user is None:
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
try:
payload = json.loads(request.body.decode('utf-8'))
except Exception:
return JsonResponse({"status": "error", "message": "JSON无效"}, status=400)
try:
existing = get_by_id(doc_id)
user_existing = get_user_by_id(request_user)
if not existing:
return JsonResponse({"status": "error", "message": "数据不存在"}, status=404)
is_admin = (request.session.get("permission", 1) == 0)
is_admin = (user_existing.get('permission')) == 0
is_owner = str(existing.get("writer_id", "")) == str(request.session.get("user_id"))
if not (is_admin or is_owner):
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
@@ -315,14 +332,14 @@ def upload_page(request):
# 上传并识别(不入库)
@require_http_methods(["POST"])
def upload(request):
# if not request.session.get("user_id"):
# fallback_uid = request.POST.get("user_id") or request.GET.get("user_id")
# if fallback_uid:
# request.session["user_id"] = fallback_uid
# request.session.setdefault("permission", 1)
# else:
# return JsonResponse({"status": "error", "message": "未登录"}, status=401)
#
if request.session.get("user_id") is None:
fallback_uid = request.POST.get("user_id") or request.GET.get("user_id")
if fallback_uid:
request.session["user_id"] = fallback_uid
request.session.setdefault("permission", 1)
else:
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
file = request.FILES.get("file")
if not file:
return JsonResponse({"status": "error", "message": "未选择文件"}, status=400)
@@ -357,18 +374,18 @@ def upload(request):
# 确认并入库
@require_http_methods(["POST"])
def confirm(request):
# if not request.session.get("user_id"):
# # 允许从payload中带入user_id作为后备便于前端已知用户时继续操作
# try:
# payload_for_uid = json.loads(request.body.decode("utf-8"))
# except Exception:
# payload_for_uid = {}
# fb_uid = (payload_for_uid or {}).get("user_id")
# if fb_uid:
# request.session["user_id"] = fb_uid
# request.session.setdefault("permission", 1)
# else:
# return JsonResponse({"status": "error", "message": "未登录"}, status=401)
if request.session.get("user_id") is None:
# 允许从payload中带入user_id作为后备便于前端已知用户时继续操作
try:
payload_for_uid = json.loads(request.body.decode("utf-8"))
except Exception:
payload_for_uid = {}
fb_uid = (payload_for_uid or {}).get("user_id")
if fb_uid:
request.session["user_id"] = fb_uid
request.session.setdefault("permission", 1)
else:
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
try:
payload = json.loads(request.body.decode("utf-8"))
@@ -401,6 +418,7 @@ def manage_page(request):
if session_user_id is None:
from django.shortcuts import redirect
return redirect("/accounts/login/")
# is_admin = (request.session.get("permission", 1) == 0)
raw_results = search_all()
# if not is_admin:

View File

@@ -1,89 +1,110 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>紫金·稷下薪火·云枢智海师生成果共创系统</title>
<style>
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; background: #f5f7fb; color:#1f2328; }
.container { max-width: 1120px; margin: 6vh auto; padding: 24px; }
.card { background: #fff; border-radius: 14px; box-shadow: 0 10px 24px rgba(31,35,40,0.08); padding: 20px; transition: transform .25s ease, box-shadow .25s ease; }
.card:hover { transform: translateY(-2px); box-shadow: 0 14px 28px rgba(31,35,40,0.10); }
.grid { display:grid; grid-template-columns: repeat(2, 1fr); gap:16px; }
.grid-3 { display:grid; grid-template-columns: repeat(3, 1fr); gap:16px; }
h2 { margin:0 0 8px; font-weight:600; }
.muted { color:#6b7280; font-size:12px; }
.header { display:flex; align-items:center; justify-content:space-between; margin-bottom:12px; }
.badge { background:#eef2ff; color:#3730a3; border-radius:999px; padding:4px 10px; font-size:12px; }
.legend { display:flex; gap:12px; align-items:center; }
.legend .dot { width:8px; height:8px; border-radius:50%; display:inline-block; }
.topbar { background: linear-gradient(90deg,#4f46e5,#06b6d4); color:#fff; padding:14px 24px; box-shadow: 0 6px 18px rgba(31,35,40,0.12); }
.topbar h1 { margin:0; font-size:18px; font-weight:600; letter-spacing:0.5px; }
.nav-item a { display:block; padding:8px 10px; border-radius:8px; transition: background .2s ease, transform .2s ease; }
.nav-item a:hover { background:#eff6ff; transform: translateX(2px); }
.btn { padding:8px 12px; border:none; border-radius:8px; cursor:pointer; }
.btn-primary { background:#4f46e5; color:#fff; }
.btn-primary:hover { filter: brightness(1.05); }
</style>
{% csrf_token %}
<!-- CSRF token to assist logout POST via cookie/header -->
<meta charset="UTF-8" />
<title>固定左侧栏目</title>
<style>
.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: 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>
</head>
<body>
<div class="topbar"><h1>紫金·稷下薪火·云枢智海师生成果共创系统</h1></div>
<div class="container" style="display:flex; gap:16px;">
<aside style="width:240px; background:#fff; border-radius:14px; box-shadow: 0 10px 24px rgba(31,35,40,0.08); padding:16px; height: fit-content;">
<h3 style="margin-top:0; font-size:16px;">导航</h3>
<nav>
<ul style="list-style:none; padding-left:0; line-height:1.9;">
<li class="nav-item"><a href="/" style="text-decoration:none; color:#1677ff;">主页</a></li>
<li class="nav-item"><a href="/elastic/upload-page/" style="text-decoration:none; color:#1677ff;">图片上传与识别</a></li>
<li class="nav-item"><a href="/elastic/manage/" style="text-decoration:none; color:#1677ff;">数据管理(管理员)</a></li>
</ul>
</nav>
<hr/>
<button id="logoutBtn" style="padding:8px 12px; width:100%; background:#ff4d4f; color:#fff; border:none; border-radius:6px; cursor:pointer;">退出登录</button>
<div id="logoutMsg" class="muted" style="margin-top:8px;"></div>
</aside>
<div style="flex:1; display:flex; flex-direction:column; gap:16px;">
<div class="card">
<div class="header">
<h2>数据概览</h2>
<div style="display:flex; gap:8px; align-items:center;">
<span class="badge">用户:{{ user_id }}</span>
{% if is_admin %}
<button id="triggerAnalyze" class="btn btn-primary">手动开始分析</button>
{% endif %}
</div>
</div>
<div class="grid-3">
<div>
<div class="legend"><span class="dot" style="background:#4f46e5;"></span><span class="muted">最近十天录入</span></div>
<canvas id="chartDays" height="140"></canvas>
</div>
<div>
<div class="legend"><span class="dot" style="background:#16a34a;"></span><span class="muted">最近十周录入</span></div>
<canvas id="chartWeeks" height="140"></canvas>
</div>
<div>
<div class="legend"><span class="dot" style="background:#ea580c;"></span><span class="muted">最近十个月录入</span></div>
<canvas id="chartMonths" height="140"></canvas>
</div>
</div>
</div>
<div class="grid">
<div class="card">
<div class="header"><h2>近1个月成果类型</h2></div>
<canvas id="pie1m" height="200"></canvas>
</div>
<div class="card">
<div class="header"><h2>近12个月成果类型</h2></div>
<canvas id="pie12m" height="200"></canvas>
</div>
</div>
<!-- 左侧固定栏目 -->
<div class="sidebar">
<div class="user-id">
<h3>用户ID{{ user_id }}</h3>
</div>
<script>
<div class="navigation-links">
<a href="/">主页</a>
<a href="{% url 'elastic:upload_page' %}">图片上传与识别</a>
<a href="{% url 'elastic:manage_page' %}">数据管理(管理员)</a>
<button id="logoutBtn">退出登录</button>
<div id="logoutMsg"></div>
{% csrf_token %}
</div>
</div>
<!-- 主内容区域 -->
<div class="main-content">
<h1>欢迎来到系统</h1>
<p>这里是一大堆内容……</p>
<p style="height: 200vh;">滚动试试看,左边菜单不会消失哦!✨</p>
</div>
<!-- 登出脚本(保持不变) -->
<script>
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
@@ -114,94 +135,6 @@
msg.textContent = e.message || '发生错误';
}
});
</script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
async function loadAnalytics() {
const resp = await fetch('/elastic/analytics/overview/');
const d = await resp.json();
if (!resp.ok || d.status !== 'success') return;
const data = d.data || {};
renderLine('chartDays', data.last_10_days || [], '#4f46e5');
renderLine('chartWeeks', data.last_10_weeks || [], '#16a34a');
renderLine('chartMonths', data.last_10_months || [], '#ea580c');
renderPie('pie1m', data.type_pie_1m || []);
renderPie('pie12m', data.type_pie_12m || []);
}
const btn = document.getElementById('triggerAnalyze');
if (btn) {
btn.addEventListener('click', async () => {
btn.disabled = true;
btn.textContent = '分析中…';
try {
const resp = await fetch('/elastic/analytics/overview/?force=1');
const d = await resp.json();
if (!resp.ok || d.status !== 'success') throw new Error('分析失败');
window.location.reload();
} catch (e) {
btn.textContent = '重试';
btn.disabled = false;
}
});
}
function hexWithAlpha(hex, alphaHex) {
if (!hex || !hex.startsWith('#')) return hex;
if (hex.length === 7) return hex + alphaHex;
return hex;
}
function renderLine(id, items, color) {
const ctx = document.getElementById(id);
const labels = items.map(x => x.label);
const values = items.map(x => x.count);
new Chart(ctx, {
type: 'line',
data: {
labels,
datasets: [{
data: values,
borderColor: color,
backgroundColor: hexWithAlpha(color, '26'),
tension: 0.25,
fill: true,
pointRadius: 3,
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
animation: { duration: 800, easing: 'easeOutQuart' },
scales: {
x: { grid: { display: false } },
y: { grid: { color: 'rgba(31,35,40,0.06)' }, beginAtZero: true }
}
}
});
}
function renderPie(id, items) {
const ctx = document.getElementById(id);
const labels = items.map(x => x.type);
const values = items.map(x => x.count);
const colors = ['#2563eb','#22c55e','#f59e0b','#ef4444','#a855f7','#06b6d4','#84cc16','#ec4899','#475569','#d946ef'];
new Chart(ctx, {
type: 'doughnut',
data: {
labels,
datasets: [{ data: values, backgroundColor: colors.slice(0, labels.length) }]
},
options: {
responsive: true,
animation: { duration: 900, easing: 'easeOutQuart' },
plugins: { legend: { position: 'bottom' } }
}
});
}
loadAnalytics();
</script>
</div>
</script>
</body>
</html>