数据可视化补充
This commit is contained in:
@@ -2,24 +2,41 @@
|
|||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>主页</title>
|
<title>紫金·稷下薪火·云枢智海师生成果共创系统</title>
|
||||||
<style>
|
<style>
|
||||||
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; background: #fafafa; }
|
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; background: #f5f7fb; color:#1f2328; }
|
||||||
.container { max-width: 720px; margin: 8vh auto; padding: 24px; }
|
.container { max-width: 1120px; margin: 6vh auto; padding: 24px; }
|
||||||
.card { background: #fff; border-radius: 10px; box-shadow: 0 6px 18px rgba(0,0,0,0.06); 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>
|
</style>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<!-- CSRF token to assist logout POST via cookie/header -->
|
<!-- CSRF token to assist logout POST via cookie/header -->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div class="topbar"><h1>紫金·稷下薪火·云枢智海师生成果共创系统</h1></div>
|
||||||
<div class="container" style="display:flex; gap:16px;">
|
<div class="container" style="display:flex; gap:16px;">
|
||||||
<aside style="width:220px; background:#fff; border-radius:10px; box-shadow: 0 6px 18px rgba(0,0,0,0.06); padding:16px; height: fit-content;">
|
<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>
|
<h3 style="margin-top:0; font-size:16px;">导航</h3>
|
||||||
<nav>
|
<nav>
|
||||||
<ul style="list-style:none; padding-left:0; line-height:1.9;">
|
<ul style="list-style:none; padding-left:0; line-height:1.9;">
|
||||||
<li><a href="/" style="text-decoration:none; color:#1677ff;">主页</a></li>
|
<li class="nav-item"><a href="/" style="text-decoration:none; color:#1677ff;">主页</a></li>
|
||||||
<li><a href="/elastic/upload-page/" 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><a href="/elastic/manage/" 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>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<hr/>
|
<hr/>
|
||||||
@@ -27,11 +44,43 @@
|
|||||||
<div id="logoutMsg" class="muted" style="margin-top:8px;"></div>
|
<div id="logoutMsg" class="muted" style="margin-top:8px;"></div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div class="card" style="flex:1;">
|
<div style="flex:1; display:flex; flex-direction:column; gap:16px;">
|
||||||
<h2>主页(留白)</h2>
|
<div class="card">
|
||||||
<p>用户ID:{{ user_id }}</p>
|
<div class="header">
|
||||||
<p>这里留白即可,主页不由当前实现负责。</p>
|
<h2>数据概览</h2>
|
||||||
<p class="muted">提示:已使用安全的会话 Cookie 管理登录状态。</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -66,6 +115,92 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
from elastic.es_connect import get_user_by_id
|
||||||
|
|
||||||
|
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
@@ -11,7 +12,14 @@ def home(request):
|
|||||||
|
|
||||||
# Show user_id (prefer query param if present, but don't trust it)
|
# Show user_id (prefer query param if present, but don't trust it)
|
||||||
user_id_qs = request.GET.get("user_id")
|
user_id_qs = request.GET.get("user_id")
|
||||||
|
uid = user_id_qs or session_user_id
|
||||||
|
perm = request.session.get("permission")
|
||||||
|
if perm is None and uid is not None:
|
||||||
|
u = get_user_by_id(uid)
|
||||||
|
perm = (u or {}).get("permission", 1)
|
||||||
|
request.session["permission"] = perm
|
||||||
context = {
|
context = {
|
||||||
"user_id": user_id_qs or session_user_id,
|
"user_id": uid,
|
||||||
|
"is_admin": (perm == 0),
|
||||||
}
|
}
|
||||||
return render(request, "main/home.html", context)
|
return render(request, "main/home.html", context)
|
||||||
Reference in New Issue
Block a user