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 fcff28ab60 feat: Studio 概览/分析补上阅读量 + 详情页 meta 行改版
- StudioOverview 新增 TotalViews 字段,GetOverviewByUserID 查询含 SUM(views_count)
- 新增 ViewTrendStore 接口和 AggregateViewsByAuthor,UNION ALL 合并已登录+访客阅读日志按日期聚合
- 数据分析页面新增阅读量趋势折线图(绿色,Chart.js)
- 概览和数据分析页面新增总阅读量统计卡片
- 详情页 meta 行改为标签格式:作者/阅读/点赞/收藏/评论/发布时间
- 移除详情页公开状态徽章,已锁定+理由仅作者可见
2026-06-02 23:26:48 +08:00

285 lines
11 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 class="stat-card">
<div class="stat-card-icon">👁</div>
<div class="stat-card-body">
<span class="stat-card-value">{{.Overview.TotalViews}}</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 class="trends-card">
<h3 class="trends-card-title">阅读量</h3>
<div class="trends-chart-wrap">
<canvas id="viewsChart"></canvas>
</div>
<div class="trends-loading" id="viewsLoading">加载中...</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)' },
views: { border: '#10b981', bg: 'rgba(16, 185, 129, 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');
showLoading('viewsLoading');
var canvasIds = ['energyChart', 'commentsChart', 'likesChart', 'favoritesChart', 'viewsChart'];
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' },
{ id: 'viewsChart', key: 'views', color: chartColors.views, label: '阅读', loading: 'viewsLoading' }
];
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>