爆改了数据可视化

This commit is contained in:
2025-11-17 01:07:52 +08:00
parent ec7bc64bfa
commit cb28d45cd1
6 changed files with 369 additions and 175 deletions

View File

@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8" />
<title>数据管理系统</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<style>
body {
margin: 0;
@@ -154,42 +155,31 @@
<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>
<h2>主页</h2>
<span class="badge">用户:{{ user_id }}</span>
</div>
<div class="muted">数据可视化概览:录入量变化、类型占比、类型变化、最近活动</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 class="header"><h3>录入量变化近90天</h3></div>
<div id="chartTrend" style="width:100%;height:320px;"></div>
</div>
<div class="card">
<div class="header"><h2>近12个月成果类型</h2></div>
<canvas id="pie12m" height="200"></canvas>
<div class="header"><h3>类型占比近30天</h3></div>
<div id="chartTypes" style="width:100%;height:320px;"></div>
</div>
<div class="card">
<div class="header"><h3>类型变化近180天按周</h3></div>
<div id="chartTypesTrend" style="width:100%;height:320px;"></div>
</div>
<div class="card">
<div class="header"><h3>最近活动近7天</h3></div>
<ul id="recentList" style="list-style:none;padding:0;margin:0;"></ul>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// 获取CSRF令牌的函数
function getCookie(name) {
@@ -256,88 +246,101 @@
}
});
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 || []);
}
function fetchJSON(url){ return fetch(url, {credentials:'same-origin'}).then(r=>r.json()); }
function qs(params){ const u = new URLSearchParams(params); return u.toString(); }
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;
}
const trendChart = echarts.init(document.getElementById('chartTrend'));
const typesChart = echarts.init(document.getElementById('chartTypes'));
const typesTrendChart = echarts.init(document.getElementById('chartTypesTrend'));
async function loadTrend(){
const url = '/elastic/analytics/trend/?' + qs({ from:'now-90d', to:'now', interval:'day' });
const res = await fetchJSON(url);
if(res.status!=='success') return;
const buckets = res.data || [];
const x = buckets.map(b=>b.key_as_string||'');
const y = buckets.map(b=>b.doc_count||0);
trendChart.setOption({
tooltip:{trigger:'axis'},
xAxis:{type:'category', data:x},
yAxis:{type:'value'},
series:[{ type:'line', areaStyle:{}, data:y, smooth:true, color:'#4f46e5' }]
});
}
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' } }
}
async function loadTypes(){
const url = '/elastic/analytics/types/?' + qs({ from:'now-30d', to:'now', size:10 });
const res = await fetchJSON(url);
if(res.status!=='success') return;
const buckets = res.data || [];
const data = buckets.map(b=>({ name: String(b.key||'未知'), value: b.doc_count||0 }));
typesChart.setOption({
tooltip:{trigger:'item'},
legend:{type:'scroll'},
series:[{ type:'pie', radius:['40%','70%'], data }]
});
}
loadAnalytics();
async function loadTypesTrend(){
const url = '/elastic/analytics/types_trend/?' + qs({ from:'now-180d', to:'now', interval:'week', size:6 });
const res = await fetchJSON(url);
if(res.status!=='success') return;
const rows = res.data || [];
const x = rows.map(r=>r.key_as_string||'');
const typeSet = new Set();
rows.forEach(r=> (r.types||[]).forEach(t=> typeSet.add(String(t.key||'未知'))));
const types = Array.from(typeSet);
const series = types.map(tp=>({
name: tp,
type:'line',
smooth:true,
data: rows.map(r=>{
const b = (r.types||[]).find(x=>String(x.key||'')===tp);
return b? b.doc_count||0 : 0;
})
}));
typesTrendChart.setOption({
tooltip:{trigger:'axis'},
legend:{type:'scroll'},
xAxis:{type:'category', data:x},
yAxis:{type:'value'},
series
});
}
function formatTime(t){
try{
const d = new Date(t);
if(String(d) !== 'Invalid Date'){
const pad = n=> String(n).padStart(2,'0');
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
}catch(e){}
return t||'';
}
async function loadRecent(){
const listEl = document.getElementById('recentList');
const url = '/elastic/analytics/recent/?' + qs({ from:'now-7d', to:'now', limit:10 });
const res = await fetchJSON(url);
if(res.status!=='success') return;
const items = res.data || [];
listEl.innerHTML = '';
items.forEach(it=>{
const li = document.createElement('li');
const t = formatTime(it.time);
const u = it.username || '';
const ty = it.type || '未知';
li.textContent = `${t}${u}${ty}`;
listEl.appendChild(li);
});
}
loadTrend();
loadTypes();
loadTypesTrend();
loadRecent();
</script>
</body>
</html>