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