Files
blog/themes/Ying/layouts/partials/heatmap.html
T
zqlit 654de1fdf0 feat(关于页): 重设计为「时间线 + 数据一览」版式,动效对齐全站 UIKit 范式
- 版式:01 关于我(属性四行 + 时间线四节点 + 三张数据卡 + 分类 Top5 条,
  全部构建期实时统计,新增 partial about-intro.html)+ 02 这个博客 + 03 走心
  评论 + 04 热力图,间距统一由 .about-sec 管,字号统一四档
- 动效:纯官方 UIKit scrollspy(与归档页同源)—— target 一律后代选择器、
  动画类用 uk-animation-slide-*-medium(50px 位移)+ repeat:true 滚回重播、
  分节标题 fade 不 repeat;删自研编排器,CSS 只留分类条宽度填充与
  prefers-reduced-motion 守卫
- 修复 PJAX 下 UIKit 不重绑的既有 bug:mypjax.js 换页后调 UIkit.update()
  (评论区/说说的 scrollspy 同样依赖此修复)
- 走心评论:骨架屏、8s 超时、sessionStorage 缓存(10 分钟)、重试按钮;
  异步渲染后手动 UIkit.update() 让卡片参与入场级联
2026-10-07 21:45:46 +08:00

202 lines
6.3 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<style>
.heatmap-scroll-wrapper {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
-ms-overflow-style: none;
scrollbar-width: none;
}
.heatmap-scroll-wrapper::-webkit-scrollbar {
display: none;
}
</style>
<div id="heatmap-wrapper" class="heatmap-scroll-wrapper" uk-scrollspy="cls: uk-animation-slide-bottom-medium; delay: 60">
<div id="heatmap" style="max-width: 800px; height: 160px; margin: 0 auto 0px auto; padding: 10px; border-radius: 10px; transition: all 0.3s ease; display: block !important; visibility: visible !important; min-width: 300px;"></div>
</div>
{{/* ★ 动效说明:散点图画在 #heatmap 里,那行内联 `visibility:visible!important`
是 ECharts 测量容器尺寸所需,不能被 scrollspy 的 hidden 覆盖。
所以 scrollspy 挂在**外层 wrapper** 上(用 cls:,target 默认为自身),
官方 slide-bottom-medium(50px 上浮)入场;图本身不受影响。
不开 repeat —— 单个大块反复重播观感太跳。 */}}
<script type="text/javascript">
function renderHeatmap() {
if (!window.echarts) {
console.warn("ECharts not loaded yet, retrying...");
setTimeout(renderHeatmap, 500);
return;
}
var chartDom = document.getElementById('heatmap');
if (!chartDom) {
console.warn("Heatmap DOM not found");
return;
}
// Dispose existing instance if any
var existing = echarts.getInstanceByDom(chartDom);
if (existing) {
existing.dispose();
}
var myChart = echarts.init(chartDom, null, { renderer: 'svg' });
// Check mobile state
var isMobile = window.innerWidth < 768;
// Handle resize with debouncing and re-render for responsive range
var resizeTimer;
window.addEventListener('resize', function() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function() {
var newIsMobile = window.innerWidth < 768;
if (newIsMobile !== isMobile) {
// Re-render if switching between mobile/desktop to update range
renderHeatmap();
} else {
myChart.resize();
}
}, 100);
});
// Data Processing
var dataMap = new Map();
{{ range where .Site.RegularPages "Type" "posts" }}
var key = "{{ .Date.Format "2006-01-02" }}";
var link = "{{ .RelPermalink }}";
var title = "{{ .Title }}";
var wordCount = {{ .WordCount }};
if (!dataMap.has(key)) {
dataMap.set(key, []);
}
dataMap.get(key).push({link, title, wordCount});
{{- end -}}
var data = [];
for (const [key, value] of dataMap.entries()) {
data.push([key, value.length]);
}
// Date Range Logic
var endDate = new Date();
var startDate = new Date();
if (isMobile) {
// Mobile: Show last 5 months to fit screen
startDate.setMonth(startDate.getMonth() - 5);
} else {
// Desktop: Show last 12 months
startDate.setFullYear(startDate.getFullYear() - 1);
}
var startDateStr = echarts.format.formatTime('yyyy-MM-dd', startDate);
var endDateStr = echarts.format.formatTime('yyyy-MM-dd', endDate);
// Theme Logic
var isDark = document.documentElement.getAttribute('data-theme') === 'dark';
// Minimalist Monochrome Colors (Black/White/Gray)
// Light: Light Gray -> Black
var colorLight = ['#f0f0f0', '#d9d9d9', '#a6a6a6', '#737373', '#000000'];
// Dark: Dark Gray -> White
var colorDark = ['#1f1f1f', '#404040', '#737373', '#a6a6a6', '#ffffff'];
var rangeColors = isDark ? colorDark : colorLight;
var calendarLabelColor = isDark ? '#a6a6a6' : '#737373';
var option = {
tooltip: {
padding: 10,
backgroundColor: isDark ? '#2d2d2d' : '#ffffff',
borderColor: isDark ? '#444' : '#eee',
borderWidth: 1,
textStyle: {
color: isDark ? '#eee' : '#333',
fontSize: 12
},
formatter: function (p) {
const date = p.data[0];
const posts = dataMap.get(date);
if (!posts) return;
var content = `<div style="font-weight:bold; margin-bottom:5px;">${date}</div>`;
content += `<div style="max-height:150px; overflow-y:auto;">`;
for (const post of posts) {
content += `<div style="margin-bottom:2px;">`;
content += `<a href="${post.link}" target="_blank" style="color:${isDark ? '#fff' : '#000'}; text-decoration:none;">${post.title}</a>`;
content += `</div>`;
}
content += `</div>`;
return content;
}
},
visualMap: {
min: 0,
max: 4,
type: 'piecewise',
orient: 'horizontal',
left: 'center',
top: 0,
show: false,
inRange: {
color: rangeColors.slice(1)
}
},
calendar: {
top: 30,
left: 'center',
range: [startDateStr, endDateStr],
cellSize: ['auto', 13],
splitLine: { show: false },
itemStyle: {
color: isDark ? '#1f1f1f' : '#f0f0f0',
borderWidth: 2,
borderColor: isDark ? '#121212' : '#ffffff'
},
yearLabel: { show: false },
monthLabel: {
nameMap: 'CN',
color: calendarLabelColor,
fontSize: 12
},
dayLabel: {
show: true,
firstDay: 1,
nameMap: ['日', '一', '二', '三', '四', '五', '六'],
color: calendarLabelColor,
fontSize: 10
}
},
series: {
type: 'heatmap',
coordinateSystem: 'calendar',
data: data,
itemStyle: {
borderRadius: 2
}
}
};
myChart.setOption(option);
}
// Initial render
if (window.echarts) {
renderHeatmap();
} else if (window.optionalAssetUrls && window.optionalAssetUrls.echarts && window.loadScriptOnce) {
window.loadScriptOnce(window.optionalAssetUrls.echarts, 'echarts', renderHeatmap);
}
// Re-render on theme change
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type == "attributes" && mutation.attributeName == "data-theme") {
renderHeatmap();
}
});
});
observer.observe(document.documentElement, {
attributes: true
});
</script>