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

This commit is contained in:
2025-11-17 14:16:30 +08:00
10 changed files with 807 additions and 250 deletions

View File

@@ -17,9 +17,8 @@ class ElasticConfig(AppConfig):
return
# 延迟导入,避免循环导入或过早加载
from .es_connect import create_index_with_mapping, start_daily_analytics_scheduler
from .es_connect import create_index_with_mapping
try:
create_index_with_mapping()
start_daily_analytics_scheduler()
except Exception as e:
print(f"❌ ES 初始化失败: {e}")

View File

@@ -9,8 +9,8 @@ from .documents import AchievementDocument, UserDocument, GlobalDocument
from .indexes import ACHIEVEMENT_INDEX_NAME, USER_INDEX_NAME, GLOBAL_INDEX_NAME
import hashlib
import time
from datetime import datetime, timezone, timedelta
import threading
from datetime import datetime, timezone
import json
# 使用环境变量配置ES连接默认为本机
_ES_URL = os.environ.get('ELASTICSEARCH_URL', 'http://localhost:9200')
@@ -309,67 +309,193 @@ def search_by_any_field(keyword):
print(f"模糊搜索失败: {str(e)}")
return []
ANALYTICS_CACHE = {"data": None, "ts": 0}
def _compute_hist(range_gte: str, interval: str, fmt: str):
from elasticsearch_dsl import Search
s = AchievementDocument.search()
s = s.filter('range', time={'gte': range_gte, 'lte': 'now'})
s = s.extra(size=0)
s.aggs.bucket('b', 'date_histogram', field='time', calendar_interval=interval, format=fmt, min_doc_count=0)
resp = s.execute()
buckets = getattr(resp.aggs, 'b').buckets
return [{"label": b.key_as_string, "count": b.doc_count} for b in buckets]
def _compute_type_counts(range_gte: str, types: list):
counts = []
def _type_filters_from_list(limit: int = None):
try:
types = get_type_list()
except Exception:
types = ['软著', '专利', '奖状']
if isinstance(limit, int) and limit > 0:
types = types[:limit]
filters = {}
for t in types:
s = AchievementDocument.search()
s = s.filter('range', time={'gte': range_gte, 'lte': 'now'})
s = s.query('match_phrase', data=str(t))
total = s.count()
counts.append({"type": str(t), "count": int(total)})
return counts
key = str(t)
# 精确匹配键与值之间的关系,避免其它字段中的同名值造成误匹配
pattern = f'*"数据类型": "{key}"*'
filters[key] = {"wildcard": {"data.keyword": {"value": pattern}}}
return filters
def compute_analytics():
types = get_type_list()
days = _compute_hist('now-10d/d', 'day', 'yyyy-MM-dd')
weeks = _compute_hist('now-10w/w', 'week', 'yyyy-ww')
months = _compute_hist('now-10M/M', 'month', 'yyyy-MM')
pie_1m = _compute_type_counts('now-1M/M', types)
pie_12m = _compute_type_counts('now-12M/M', types)
return {
"last_10_days": days[-10:],
"last_10_weeks": weeks[-10:],
"last_10_months": months[-10:],
"type_pie_1m": pie_1m,
"type_pie_12m": pie_12m,
}
def analytics_trend(gte: str = None, lte: str = None, interval: str = "day"):
try:
search = AchievementDocument.search()
body = {
"size": 0,
"aggs": {
"trend": {
"date_histogram": {
"field": "time",
"calendar_interval": interval,
"min_doc_count": 0
}
}
}
}
if gte or lte:
rng = {}
if gte:
rng["gte"] = gte
if lte:
rng["lte"] = lte
body["query"] = {"range": {"time": rng}}
search = search.update_from_dict(body)
resp = search.execute()
buckets = resp.aggregations.trend.buckets if hasattr(resp, 'aggregations') else []
return [{"key_as_string": b.key_as_string, "key": b.key, "doc_count": b.doc_count} for b in buckets]
except Exception as e:
print(f"分析趋势失败: {str(e)}")
return []
def get_analytics_overview(force: bool = False):
now_ts = time.time()
if force or ANALYTICS_CACHE["data"] is None or (now_ts - ANALYTICS_CACHE["ts"]) > 3600:
ANALYTICS_CACHE["data"] = compute_analytics()
ANALYTICS_CACHE["ts"] = now_ts
return ANALYTICS_CACHE["data"]
def analytics_types(gte: str = None, lte: str = None, size: int = 10):
try:
filters = _type_filters_from_list(limit=size)
body = {
"size": 0,
"aggs": {
"by_type": {
"filters": {
"filters": filters
}
}
}
}
if gte or lte:
rng = {}
if gte:
rng["gte"] = gte
if lte:
rng["lte"] = lte
body["query"] = {"range": {"time": rng}}
resp = es.search(index=DATA_INDEX_NAME, body=body)
buckets = resp.get("aggregations", {}).get("by_type", {}).get("buckets", {})
out = []
for k, v in buckets.items():
try:
out.append({"key": k, "doc_count": int(v.get("doc_count", 0))})
except Exception:
out.append({"key": str(k), "doc_count": 0})
return out
except Exception as e:
print(f"分析类型占比失败: {str(e)}")
return []
def _seconds_until_hour(h: int):
now = datetime.now()
tgt = now.replace(hour=h, minute=0, second=0, microsecond=0)
if tgt <= now:
tgt = tgt + timedelta(days=1)
return max(0, int((tgt - now).total_seconds()))
def analytics_types_trend(gte: str = None, lte: str = None, interval: str = "week", size: int = 8):
try:
filters = _type_filters_from_list(limit=size)
body = {
"size": 0,
"aggs": {
"by_interval": {
"date_histogram": {
"field": "time",
"calendar_interval": interval,
"min_doc_count": 0
},
"aggs": {
"by_type": {
"filters": {"filters": filters}
}
}
}
}
}
if gte or lte:
rng = {}
if gte:
rng["gte"] = gte
if lte:
rng["lte"] = lte
body["query"] = {"range": {"time": rng}}
resp = es.search(index=DATA_INDEX_NAME, body=body)
by_interval = resp.get("aggregations", {}).get("by_interval", {}).get("buckets", [])
out = []
for ib in by_interval:
t_buckets = ib.get("by_type", {}).get("buckets", {})
types_arr = []
for k, v in t_buckets.items():
types_arr.append({"key": k, "doc_count": int(v.get("doc_count", 0))})
out.append({
"key_as_string": ib.get("key_as_string"),
"key": ib.get("key"),
"doc_count": ib.get("doc_count", 0),
"types": types_arr
})
return out
except Exception as e:
print(f"分析类型变化失败: {str(e)}")
return []
def start_daily_analytics_scheduler():
def _run_and_reschedule():
try:
get_analytics_overview(force=True)
except Exception as e:
print(f"分析任务失败: {e}")
finally:
threading.Timer(24 * 3600, _run_and_reschedule).start()
delay = _seconds_until_hour(3)
threading.Timer(delay, _run_and_reschedule).start()
def analytics_recent(limit: int = 10, gte: str = None, lte: str = None):
try:
def _extract_type(s: str):
if not s:
return ""
try:
obj = json.loads(s)
if isinstance(obj, dict):
v = obj.get("数据类型")
if isinstance(v, str) and v:
return v
except Exception:
pass
try:
m = re.search(r'"数据类型"\s*:\s*"([^"]+)"', s)
if m:
return m.group(1)
except Exception:
pass
return ""
search = AchievementDocument.search()
body = {
"size": max(1, min(limit, 100)),
"sort": [{"time": {"order": "desc"}}]
}
if gte or lte:
rng = {}
if gte:
rng["gte"] = gte
if lte:
rng["lte"] = lte
body["query"] = {"range": {"time": rng}}
search = search.update_from_dict(body)
resp = search.execute()
results = []
for hit in resp:
w = getattr(hit, 'writer_id', '')
uname = None
try:
uname_lookup = get_user_by_id(w)
uname = (uname_lookup or {}).get("username")
except Exception:
uname = None
if not uname:
try:
uname_lookup = get_user_by_id(int(w))
uname = (uname_lookup or {}).get("username")
except Exception:
uname = None
tval = _extract_type(getattr(hit, 'data', ''))
results.append({
"_id": hit.meta.id,
"writer_id": w,
"username": uname or "",
"type": tval or "",
"time": getattr(hit, 'time', None)
})
return results
except Exception as e:
print(f"获取最近活动失败: {str(e)}")
return []
def write_user_data(user_data):
"""

View File

@@ -1,5 +1,5 @@
INDEX_NAME = "wordsearch266666"
INDEX_NAME = "wordsearch2666661"
USER_NAME = "users11111"
ACHIEVEMENT_INDEX_NAME = INDEX_NAME
USER_INDEX_NAME = USER_NAME
GLOBAL_INDEX_NAME = "global11111"
GLOBAL_INDEX_NAME = "global11111111211"

View File

@@ -29,13 +29,13 @@
.user-id {
text-align: center;
margin-bottom: auto;
margin-bottom: 0px;
}
.sidebar h3 {
margin-top: 0;
font-size: 18px;
color: #ff79c6;
color: #add8e6;
text-align: center;
margin-bottom: 20px;
}
@@ -103,6 +103,7 @@
max-width: 120px;
border: 1px solid #eee;
border-radius: 6px;
cursor: pointer; /* 添加指针样式 */
}
.btn {
padding: 6px 10px;
@@ -230,6 +231,47 @@
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>
@@ -306,6 +348,12 @@
</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) {
@@ -327,6 +375,11 @@ 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 = '';
@@ -476,7 +529,7 @@ function renderTable(data) {
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='图片加载失败'" />` : '无图片'}
${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>
@@ -722,6 +775,30 @@ document.getElementById('logoutBtn').addEventListener('click', async () => {
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>

View File

@@ -10,7 +10,7 @@
background: #fafafa;
}
/* 导航栏样式 */
/* 导航栏样式 - 保持原有样式 */
.sidebar {
position: fixed;
top: 0;
@@ -29,13 +29,13 @@
.user-id {
text-align: center;
margin-bottom: auto;
margin-bottom: 0px;
}
.sidebar h3 {
margin-top: 0;
font-size: 18px;
color: #ff79c6;
color: #add8e6;
text-align: center;
margin-bottom: 20px;
}
@@ -68,42 +68,207 @@
background-color: rgba(139, 233, 253, 0.2);
}
/* 主内容区 */
/* 主内容区 - 改进后的样式 */
.main-content {
margin-left: 200px;
padding: 20px;
color: #333;
}
/* 原有样式保持不变 */
.container {
max-width: 900px;
margin: 6vh auto;
max-width: 1200px;
margin: 0 auto;
background: #fff;
border-radius: 10px;
box-shadow: 0 6px 18px rgba(0,0,0,0.06);
border-radius: 14px;
box-shadow: 0 10px 24px rgba(31,35,40,0.08);
padding: 24px;
}
.row { display: flex; gap: 16px; }
.col { flex: 1; }
textarea {
width: 100%;
min-height: 260px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.header h2 {
margin: 0;
color: #1e293b;
}
.header p {
margin: 5px 0 0 0;
color: #64748b;
font-size: 14px;
}
img { max-width: 100%; border: 1px solid #eee; border-radius: 6px; }
.btn {
padding: 8px 12px;
border: none;
border-radius: 6px;
cursor: pointer;
.upload-section {
background: #f8fafc;
border: 2px dashed #cbd5e1;
border-radius: 12px;
padding: 32px;
text-align: center;
transition: all 0.3s ease;
margin-bottom: 24px;
}
.upload-section:hover {
border-color: #4f46e5;
background: #f1f5f9;
}
.upload-section.drag-over {
border-color: #4f46e5;
background: #e0e7ff;
}
.upload-section input[type="file"] {
margin: 15px 0;
}
.btn {
padding: 10px 16px;
border: none;
border-radius: 8px;
cursor: pointer;
margin: 0 4px;
font-size: 14px;
transition: all 0.2s ease;
}
.btn-primary {
background: #4f46e5;
color: #fff;
}
.btn-primary:hover {
background: #4338ca;
}
.btn-secondary {
background: #e2e8f0;
color: #334155;
}
.btn-secondary:hover {
background: #cbd5e1;
}
.btn-danger {
background: #ef4444;
color: #fff;
}
.btn-danger:hover {
background: #dc2626;
}
.preview-container {
display: flex;
gap: 24px;
margin: 24px 0;
}
@media (max-width: 768px) {
.preview-container {
flex-direction: column;
}
}
.preview-box {
flex: 1;
text-align: center;
}
.preview-box h3 {
margin-top: 0;
color: #334155;
}
.preview-box img {
max-width: 100%;
max-height: 300px;
border: 1px solid #e2e8f0;
border-radius: 8px;
object-fit: contain;
}
.result-box {
flex: 1;
}
.result-box h3 {
margin-top: 0;
color: #334155;
}
.form-controls {
display: flex;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
#kvForm {
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 12px;
max-height: 300px;
overflow: auto;
margin-bottom: 12px;
background: white;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr auto;
gap: 8px;
margin-bottom: 6px;
}
.form-row input {
padding: 8px;
border: 1px solid #cbd5e1;
border-radius: 4px;
}
#resultBox {
width: 100%;
min-height: 200px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 14px;
padding: 12px;
border: 1px solid #e2e8f0;
border-radius: 8px;
resize: vertical;
box-sizing: border-box;
}
.status-message {
padding: 10px;
margin: 10px 0;
border-radius: 6px;
display: none;
}
.status-message.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status-message.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.action-buttons {
margin-top: 16px;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.btn-primary { background: #1677ff; color: #fff; }
.btn-secondary { background: #f0f0f0; }
.muted { color: #666; font-size: 12px; }
.error { color: #d14343; }
.success { color: #179957; }
</style>
</head>
<body>
@@ -123,38 +288,46 @@
<!-- 主内容区域 -->
<div class="main-content">
<div class="container">
<h2>图片上传与识别</h2>
<p class="muted">选择图片后上传,服务端调用大模型解析为可编辑的 JSON再确认入库。</p>
<form id="uploadForm" enctype="multipart/form-data">
{% csrf_token %}
<input type="file" id="fileInput" name="file" accept="image/*" />
<button type="submit" class="btn btn-primary">上传并识别</button>
<span id="uploadMsg" class="muted"></span>
</form>
<div class="row" style="margin-top:16px;">
<div class="col">
<h4>图片预览</h4>
<img id="preview" alt="预览" />
</div>
<div class="col">
<h4>识别结果(可编辑)</h4>
<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>
</div>
<div id="kvForm" style="border:1px solid #eee; border-radius:6px; padding:8px; max-height:300px; overflow:auto;"></div>
<div style="margin-top:8px;">
<textarea id="resultBox" placeholder="识别结果JSON将显示在这里"></textarea>
</div>
</div>
<div class="header">
<div>
<h2>图片上传与识别</h2>
<p>选择图片后上传,服务端调用大模型解析为可编辑的 JSON再确认入库。</p>
</div>
</div>
<div style="margin-top:16px;">
<button id="confirmBtn" class="btn btn-primary" disabled>确认并入库</button>
<button id="clearBtn" class="btn btn-secondary" type="button">清空</button>
<span id="confirmMsg" class="muted"></span>
<div class="upload-section" id="dropArea">
<h3>上传图片</h3>
<p>点击下方按钮选择图片,或拖拽图片到此区域</p>
<form id="uploadForm" enctype="multipart/form-data">
{% csrf_token %}
<input type="file" id="fileInput" name="file" accept="image/*" required />
<br>
<button type="submit" class="btn btn-primary">上传并识别</button>
</form>
<div class="status-message" id="uploadMsg"></div>
</div>
<div class="preview-container">
<div class="preview-box">
<h3>图片预览</h3>
<img id="preview" alt="预览" />
</div>
<div class="result-box">
<h3>识别结果(可编辑)</h3>
<div class="form-controls">
<button id="addFieldBtn" class="btn btn-secondary" type="button">添加字段</button>
<button id="syncFromTextBtn" class="btn btn-secondary" type="button">从文本区刷新表单</button>
</div>
<div id="kvForm"></div>
<textarea id="resultBox" placeholder="识别结果JSON将显示在这里"></textarea>
</div>
</div>
<div class="action-buttons">
<button id="confirmBtn" class="btn btn-primary" disabled>确认并入库</button>
<button id="clearBtn" class="btn btn-secondary" type="button">清空</button>
<span id="confirmMsg" class="muted"></span>
</div>
</div>
</div>
@@ -177,15 +350,63 @@ const confirmMsg = document.getElementById('confirmMsg');
const kvForm = document.getElementById('kvForm');
const addFieldBtn = document.getElementById('addFieldBtn');
const syncFromTextBtn = document.getElementById('syncFromTextBtn');
const dropArea = document.getElementById('dropArea');
let currentImageRel = '';
// 拖拽上传功能
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropArea.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, unhighlight, false);
});
function highlight() {
dropArea.classList.add('drag-over');
}
function unhighlight() {
dropArea.classList.remove('drag-over');
}
dropArea.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length) {
fileInput.files = files;
const event = new Event('change', { bubbles: true });
fileInput.dispatchEvent(event);
}
}
// 文件选择后预览
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
preview.src = e.target.result;
};
reader.readAsDataURL(file);
}
});
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';
row.className = 'form-row';
const keyInput = document.createElement('input');
keyInput.type = 'text';
keyInput.placeholder = '字段名';
@@ -196,9 +417,17 @@ function createRow(k = '', v = '') {
valInput.value = typeof v === 'object' ? JSON.stringify(v) : (v ?? '');
const delBtn = document.createElement('button');
delBtn.type = 'button';
delBtn.className = 'btn btn-secondary';
delBtn.className = 'btn btn-danger';
delBtn.textContent = '删除';
delBtn.onclick = () => { kvForm.removeChild(row); syncTextarea(); };
delBtn.onclick = () => {
if (kvForm.children.length > 1) {
kvForm.removeChild(row);
} else {
keyInput.value = '';
valInput.value = '';
}
syncTextarea();
};
keyInput.oninput = syncTextarea;
valInput.oninput = syncTextarea;
row.appendChild(keyInput);
@@ -246,9 +475,16 @@ syncFromTextBtn.addEventListener('click', () => {
try {
const obj = JSON.parse(resultBox.value || '{}');
renderFormFromObject(obj);
uploadMsg.textContent = '已从文本区刷新表单';
uploadMsg.className = 'status-message success';
uploadMsg.style.display = 'block';
setTimeout(() => {
uploadMsg.style.display = 'none';
}, 2000);
} catch (e) {
uploadMsg.textContent = '文本区不是有效JSON';
uploadMsg.className = 'error';
uploadMsg.className = 'status-message error';
uploadMsg.style.display = 'block';
}
});
@@ -263,7 +499,8 @@ uploadForm.addEventListener('submit', async (e) => {
const file = fileInput.files[0];
if (!file) {
uploadMsg.textContent = '请选择图片文件';
uploadMsg.className = 'error';
uploadMsg.className = 'status-message error';
uploadMsg.style.display = 'block';
return;
}
@@ -282,14 +519,16 @@ uploadForm.addEventListener('submit', async (e) => {
throw new Error(data.message || '上传识别失败');
}
uploadMsg.textContent = data.message || '识别成功';
uploadMsg.className = 'success';
uploadMsg.className = 'status-message success';
uploadMsg.style.display = 'block';
preview.src = data.image_url;
renderFormFromObject(data.data || {});
currentImageRel = data.image;
confirmBtn.disabled = false;
} catch (e) {
uploadMsg.textContent = e.message || '发生错误';
uploadMsg.className = 'error';
uploadMsg.className = 'status-message error';
uploadMsg.style.display = 'block';
}
});
@@ -311,10 +550,10 @@ confirmBtn.addEventListener('click', async () => {
throw new Error(data.message || '录入失败');
}
confirmMsg.textContent = data.message || '录入成功';
confirmMsg.className = 'success';
confirmMsg.style.color = '#179957';
} catch (e) {
confirmMsg.textContent = e.message || '发生错误';
confirmMsg.className = 'error';
confirmMsg.style.color = '#d14343';
}
});
@@ -323,6 +562,7 @@ clearBtn.addEventListener('click', () => {
preview.src = '';
resultBox.value = '';
kvForm.innerHTML = '';
kvForm.appendChild(createRow()); // 保留一个空行
uploadMsg.textContent = '';
confirmMsg.textContent = '';
confirmBtn.disabled = true;

View File

@@ -29,13 +29,13 @@
.user-id {
text-align: center;
margin-bottom: auto;
margin-bottom: 0px;
}
.sidebar h3 {
margin-top: 0;
font-size: 18px;
color: #ff79c6;
color: #add8e6;
text-align: center;
margin-bottom: 20px;
}

View File

@@ -17,7 +17,6 @@ urlpatterns = [
path('search/', views.search, name='search'),
path('fuzzy-search/', views.fuzzy_search, name='fuzzy_search'),
path('all-data/', views.get_all_data, name='get_all_data'),
path('analytics/overview/', views.analytics_overview, name='analytics_overview'),
# 用户管理
path('users/', views.get_users, name='get_users'),
@@ -33,4 +32,10 @@ urlpatterns = [
# 管理页面
path('manage/', views.manage_page, name='manage_page'),
path('user_manage/', views.user_manage, name='user_manage'),
# 分析接口
path('analytics/trend/', views.analytics_trend_view, name='analytics_trend'),
path('analytics/types/', views.analytics_types_view, name='analytics_types'),
path('analytics/types_trend/', views.analytics_types_trend_view, name='analytics_types_trend'),
path('analytics/recent/', views.analytics_recent_view, name='analytics_recent'),
]

View File

@@ -14,6 +14,12 @@ from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie, csrf_protect
from .es_connect import *
from .es_connect import update_user_by_id as es_update_user_by_id, delete_user_by_id as es_delete_user_by_id
from .es_connect import (
analytics_trend as es_analytics_trend,
analytics_types as es_analytics_types,
analytics_types_trend as es_analytics_types_trend,
analytics_recent as es_analytics_recent,
)
from PIL import Image
@@ -80,16 +86,6 @@ def get_all_data(request):
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@require_http_methods(["GET"])
def analytics_overview(request):
try:
force = request.GET.get("force") == "1"
data = get_analytics_overview(force=force)
return JsonResponse({"status": "success", "data": data})
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@require_http_methods(["DELETE"])
@csrf_exempt
def delete_data(request, doc_id):
@@ -159,7 +155,11 @@ def update_data(request, doc_id):
if isinstance(v, dict):
updated["data"] = json.dumps(v, ensure_ascii=False)
else:
updated["data"] = str(v)
try:
obj = json.loads(str(v))
updated["data"] = json.dumps(obj, ensure_ascii=False)
except Exception:
updated["data"] = str(v)
success = update_by_id(doc_id, updated)
if success:
@@ -530,6 +530,67 @@ def manage_page(request):
context = {"items": results, "user_id": user_id_qs or session_user_id}
return render(request, "elastic/manage.html", context)
@require_http_methods(["GET"])
def analytics_trend_view(request):
try:
gte = request.GET.get("from")
lte = request.GET.get("to")
interval = request.GET.get("interval", "day")
data = es_analytics_trend(gte=gte, lte=lte, interval=interval)
return JsonResponse({"status": "success", "data": data})
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@require_http_methods(["GET"])
def analytics_types_view(request):
try:
gte = request.GET.get("from")
lte = request.GET.get("to")
size = request.GET.get("size")
try:
size_int = int(size) if size is not None else 10
except Exception:
size_int = 10
data = es_analytics_types(gte=gte, lte=lte, size=size_int)
return JsonResponse({"status": "success", "data": data})
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@require_http_methods(["GET"])
def analytics_types_trend_view(request):
try:
gte = request.GET.get("from")
lte = request.GET.get("to")
interval = request.GET.get("interval", "week")
size = request.GET.get("size")
try:
size_int = int(size) if size is not None else 8
except Exception:
size_int = 8
data = es_analytics_types_trend(gte=gte, lte=lte, interval=interval, size=size_int)
return JsonResponse({"status": "success", "data": data})
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@require_http_methods(["GET"])
def analytics_recent_view(request):
try:
limit = request.GET.get("limit")
gte = request.GET.get("from")
lte = request.GET.get("to")
try:
limit_int = int(limit) if limit is not None else 10
except Exception:
limit_int = 10
data = es_analytics_recent(limit=limit_int, gte=gte, lte=lte)
return JsonResponse({"status": "success", "data": data})
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
@require_http_methods(["GET"])
@ensure_csrf_cookie
def user_manage(request):