feat: 收藏夹系统 + Studio 趋势图 + 通知偏好设置
- 新增收藏夹系统 (Folder/FolderItem 模型、CRUD API、前端管理页) - 新增文章详情页收藏按钮 (书签图标、ToggleFavorite 一键收藏) - 新增 Studio 趋势图 (赋能值/评论/点赞/收藏 4 线图, 7d/30d/90d) - 新增通知偏好设置页 (评论/回复/点赞/关注 4 类开关) - 后端通知列表支持按偏好过滤 (排除已关闭的通知类型) - 下载 Chart.js 到本地静态资源 (static/js/chart.umd.min.js) - 补充收藏夹卡片、开关滑块、趋势图网格等 CSS 样式
This commit is contained in:
@ -40,26 +40,44 @@
|
||||
</div>
|
||||
|
||||
<div class="studio-section">
|
||||
<h2 class="section-title">状态分布</h2>
|
||||
<div class="status-bar">
|
||||
{{if gt .Overview.ApprovedCount 0}}
|
||||
<div class="status-bar-segment status-bar-approved" style="flex:{{.Overview.ApprovedCount}}">已发布 {{.Overview.ApprovedCount}}</div>
|
||||
{{end}}
|
||||
{{if gt .Overview.DraftCount 0}}
|
||||
<div class="status-bar-segment status-bar-draft" style="flex:{{.Overview.DraftCount}}">草稿 {{.Overview.DraftCount}}</div>
|
||||
{{end}}
|
||||
{{if gt .Overview.PendingCount 0}}
|
||||
<div class="status-bar-segment status-bar-pending" style="flex:{{.Overview.PendingCount}}">审核中 {{.Overview.PendingCount}}</div>
|
||||
{{end}}
|
||||
{{if gt .Overview.RejectedCount 0}}
|
||||
<div class="status-bar-segment status-bar-rejected" style="flex:{{.Overview.RejectedCount}}">已退回 {{.Overview.RejectedCount}}</div>
|
||||
{{end}}
|
||||
<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>
|
||||
|
||||
<div class="studio-section">
|
||||
<div class="studio-placeholder">
|
||||
<p>📈 更详细的阅读趋势、互动数据将在后续版本中推出</p>
|
||||
<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}}
|
||||
@ -68,5 +86,182 @@
|
||||
|
||||
{{template "layout/footer.html" .}}
|
||||
|
||||
<script src="/static/js/chart.umd.min.js?v={{assetV "/static/js/chart.umd.min.js"}}"></script>
|
||||
<script>
|
||||
(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>
|
||||
|
||||
Reference in New Issue
Block a user