This repository has been archived on 2026-06-21. You can view files and clone it, but cannot push or open issues or pull requests.
Files
MetaLab/templates/MetaLab-2026/html/studio/analytics.html
Victor_Jay 53f18ca6ee fix: CSP 策略移除 unsafe-inline,改用 nonce 机制
- SecurityHeaders 中间件生成随机 nonce,注入 context
- BuildPageData/BuildAdminPageData 将 CSPNonce 传递给模板
- 所有 19 个模板文件 <script> 标签添加 nonce 属性
- 移除所有内联事件处理(onclick/onerror),改用事件监听
- 移除所有 javascript:void(0) 链接,改用 # 号 + preventDefault
- utils.js 添加全局 avatar 图片加载失败回退处理
- common.js 登出按钮添加 preventDefault
- audit.js 关闭弹窗按钮改用事件监听
2026-06-02 16:10:23 +08:00

268 lines
9.8 KiB
HTML

{{template "layout/header.html" .}}
<body>
{{template "layout/nav.html" .}}
<div class="studio-wrapper">
{{template "studio/sidebar.html" .}}
<main class="studio-main">
<div class="studio-header">
<h1>数据分析</h1>
<span class="studio-subtitle">了解你的创作表现</span>
</div>
{{if .Error}}
<div class="studio-error">{{.Error}}</div>
{{else}}
<div class="stat-cards">
<div class="stat-card">
<div class="stat-card-icon">📝</div>
<div class="stat-card-body">
<span class="stat-card-value">{{.Overview.TotalPosts}}</span>
<span class="stat-card-label">文章总数</span>
</div>
</div>
<div class="stat-card stat-card-approved">
<div class="stat-card-icon"></div>
<div class="stat-card-body">
<span class="stat-card-value">{{.Overview.ApprovedCount}}</span>
<span class="stat-card-label">已发布</span>
</div>
</div>
<div class="stat-card stat-card-draft">
<div class="stat-card-icon">📄</div>
<div class="stat-card-body">
<span class="stat-card-value">{{.Overview.DraftCount}}</span>
<span class="stat-card-label">草稿</span>
</div>
</div>
</div>
<div class="studio-section">
<div class="trends-header">
<h2 class="section-title">交互趋势</h2>
<div class="trends-period">
<button class="trends-period-btn active" data-period="7d">7 天</button>
<button class="trends-period-btn" data-period="30d">30 天</button>
<button class="trends-period-btn" data-period="90d">90 天</button>
</div>
</div>
<div class="trends-grid">
<div class="trends-card">
<h3 class="trends-card-title">赋能值</h3>
<div class="trends-chart-wrap">
<canvas id="energyChart"></canvas>
</div>
<div class="trends-loading" id="energyLoading">加载中...</div>
</div>
<div class="trends-card">
<h3 class="trends-card-title">评论量</h3>
<div class="trends-chart-wrap">
<canvas id="commentsChart"></canvas>
</div>
<div class="trends-loading" id="commentsLoading">加载中...</div>
</div>
<div class="trends-card">
<h3 class="trends-card-title">点赞量</h3>
<div class="trends-chart-wrap">
<canvas id="likesChart"></canvas>
</div>
<div class="trends-loading" id="likesLoading">加载中...</div>
</div>
<div class="trends-card">
<h3 class="trends-card-title">收藏量</h3>
<div class="trends-chart-wrap">
<canvas id="favoritesChart"></canvas>
</div>
<div class="trends-loading" id="favoritesLoading">加载中...</div>
</div>
</div>
</div>
{{end}}
</main>
</div>
{{template "layout/footer.html" .}}
<script src="/static/js/chart.umd.min.js?v={{assetV "/static/js/chart.umd.min.js"}}" nonce="{{.CSPNonce}}"></script>
<script nonce="{{.CSPNonce}}">
(function () {
'use strict';
var charts = {};
var currentPeriod = '7d';
var chartColors = {
energy: { border: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
comments: { border: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
likes: { border: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
favorites: { border: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }
};
function fillDates(points, days) {
var now = new Date();
var map = {};
points.forEach(function (p) {
map[p.date] = p.value;
});
var result = [];
var labels = [];
for (var i = days - 1; i >= 0; i--) {
var d = new Date(now);
d.setDate(d.getDate() - i);
var key = d.toISOString().slice(0, 10);
labels.push(key.slice(5)); // MM-DD
result.push(map[key] || 0);
}
var valueSum = 0;
for (var j = 0; j < result.length; j++) { valueSum += result[j]; }
return { labels: labels, values: result, hasData: valueSum > 0 };
}
function createChart(canvasId, color, label) {
var ctx = document.getElementById(canvasId).getContext('2d');
return new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: label,
data: [],
borderColor: color.border,
backgroundColor: color.bg,
fill: true,
tension: 0.3,
pointRadius: 2,
pointHoverRadius: 5
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
x: {
ticks: { font: { size: 10 }, maxRotation: 45, minRotation: 45 },
grid: { display: false }
},
y: {
beginAtZero: true,
ticks: {
font: { size: 10 },
precision: 0,
callback: function (v) { return v === Math.floor(v) ? v : ''; }
},
grid: { color: 'rgba(0,0,0,0.05)' }
}
}
}
});
}
function destroyCharts() {
Object.keys(charts).forEach(function (key) {
if (charts[key]) {
charts[key].destroy();
delete charts[key];
}
});
}
function showLoading(loadingId) {
var el = document.getElementById(loadingId);
if (el) el.style.display = 'block';
}
function hideLoading(loadingId) {
var el = document.getElementById(loadingId);
if (el) el.style.display = 'none';
}
function loadTrends(period) {
currentPeriod = period;
destroyCharts();
var days = period === '30d' ? 30 : period === '90d' ? 90 : 7;
showLoading('energyLoading');
showLoading('commentsLoading');
showLoading('likesLoading');
showLoading('favoritesLoading');
var canvasIds = ['energyChart', 'commentsChart', 'likesChart', 'favoritesChart'];
canvasIds.forEach(function (id) {
var canvas = document.getElementById(id);
canvas.style.display = 'none';
});
fetch('/api/studio/trends?period=' + period)
.then(function (r) { return r.json(); })
.then(function (d) {
if (!d.success || !d.data) {
canvasIds.forEach(function (id) {
hideLoading(id.replace('Chart', 'Loading').replace('Chart', 'Loading'));
});
canvasIds.forEach(function (id) {
document.getElementById(id).style.display = 'block';
});
return;
}
var maps = [
{ id: 'energyChart', key: 'energy', color: chartColors.energy, label: '赋能值', loading: 'energyLoading' },
{ id: 'commentsChart', key: 'comments', color: chartColors.comments, label: '评论', loading: 'commentsLoading' },
{ id: 'likesChart', key: 'likes', color: chartColors.likes, label: '点赞', loading: 'likesLoading' },
{ id: 'favoritesChart', key: 'favorites', color: chartColors.favorites, label: '收藏', loading: 'favoritesLoading' }
];
maps.forEach(function (m) {
var fd = fillDates(d.data[m.key] || [], days);
hideLoading(m.loading);
var canvas = document.getElementById(m.id);
canvas.style.display = 'block';
if (!fd.hasData && fd.values.every(function (v) { return v === 0; })) {
canvas.parentElement.innerHTML = '<div class="trends-no-data">暂无数据</div>';
return;
}
charts[m.id] = createChart(m.id, m.color, m.label);
charts[m.id].data.labels = fd.labels;
charts[m.id].data.datasets[0].data = fd.values;
charts[m.id].update();
});
})
.catch(function () {
canvasIds.forEach(function (id) {
hideLoading(id.replace('Chart', 'Loading'));
document.getElementById(id).style.display = 'block';
});
});
}
// 绑定期间按钮
document.querySelectorAll('.trends-period-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
document.querySelectorAll('.trends-period-btn').forEach(function (b) {
b.classList.remove('active');
});
btn.classList.add('active');
loadTrends(btn.getAttribute('data-period'));
});
});
// 初始加载
if (document.getElementById('energyChart')) {
loadTrends('7d');
}
})();
</script>
</body>
</html>