340 lines
10 KiB
HTML
340 lines
10 KiB
HTML
<!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: auto;
|
||
}
|
||
|
||
.sidebar h3 {
|
||
margin-top: 0;
|
||
font-size: 18px;
|
||
color: #ff79c6;
|
||
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;
|
||
}
|
||
|
||
.card {
|
||
background: #fff;
|
||
border-radius: 14px;
|
||
box-shadow: 0 10px 24px rgba(31,35,40,0.08);
|
||
padding: 20px;
|
||
}
|
||
.grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 16px;
|
||
}
|
||
.grid-3 {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 16px;
|
||
}
|
||
.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;
|
||
}
|
||
.muted {
|
||
color: #6b7280;
|
||
font-size: 12px;
|
||
}
|
||
.btn {
|
||
padding: 8px 12px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
}
|
||
.btn-primary {
|
||
background: #4f46e5;
|
||
color: #fff;
|
||
}
|
||
</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' %}" onclick="return handleNavClick(this, '/');">主页</a>
|
||
<a href="{% url 'elastic:upload_page' %}" onclick="return handleNavClick(this, '/elastic/upload/');">图片上传与识别</a>
|
||
<a href="{% url 'elastic:manage_page' %}" onclick="return handleNavClick(this, '/elastic/manage/');">数据管理</a>
|
||
<button id="logoutBtn">退出登录</button>
|
||
<div id="logoutMsg"></div>
|
||
{% csrf_token %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主内容区域 -->
|
||
<div class="main-content">
|
||
<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" style="margin-top:16px;">
|
||
<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>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||
<script>
|
||
// 获取CSRF令牌的函数
|
||
function getCookie(name) {
|
||
const value = `; ${document.cookie}`;
|
||
const parts = value.split(`; ${name}=`);
|
||
if (parts.length === 2) return parts.pop().split(';').shift();
|
||
}
|
||
|
||
// 导航点击处理函数,提供备用URL
|
||
function handleNavClick(element, fallbackUrl) {
|
||
// 尝试使用Django模板生成的URL,如果失败则使用备用URL
|
||
try {
|
||
// 如果模板渲染正常,直接返回true让默认行为处理
|
||
return true;
|
||
} catch (e) {
|
||
// 如果模板渲染有问题,使用备用URL
|
||
window.location.href = fallbackUrl;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 修复用户管理链接跳转问题
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// 为用户管理链接添加事件监听器,确保正确跳转
|
||
const userManagementLink = document.querySelector('a[href*="get_users"]');
|
||
if (userManagementLink) {
|
||
userManagementLink.addEventListener('click', function(e) {
|
||
// 阻止默认行为
|
||
e.preventDefault();
|
||
|
||
// 获取备用URL
|
||
const fallbackUrl = this.getAttribute('onclick').match(/'([^']+)'/g)[1].replace(/'/g, '');
|
||
|
||
// 直接跳转到用户管理页面
|
||
window.location.href = fallbackUrl;
|
||
});
|
||
}
|
||
});
|
||
|
||
// 登出功能
|
||
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 || '发生错误';
|
||
}
|
||
});
|
||
|
||
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>
|
||
</body>
|
||
</html> |