贡献图表
- 数据可视化: 通过日历热力图形式(类似 GitHub 贡献图)展示近 365 天文章发布频率,颜色深浅反映创作密度
- 创作激励: 直观呈现内容更新趋势,帮助作者回顾创作周期,激励持续输出
- 访客感知: 向访问者展示网站内容更新频率,增强内容可信度与专业形象
- 技术集成: 基于 ECharts 实现交互式图表,支持暗黑/亮色主题自动适配,响应式布局适配不同设备
安装依赖
sh
pnpm add -D echarts
sh
npm install echarts
sh
yarn add -D echarts
sh
bun add -D echarts
新建组件
在docs\.vitepress\theme\components\
目录下新建ContributeChart.vue
文件
vue
<script setup lang="ts" name="ContributeChart">
import * as echarts from "echarts";
import { ref, watch, nextTick, computed, useTemplateRef } from "vue";
import { useData } from "vitepress";
import { formatDate, usePosts } from "vitepress-theme-teek";
const { isDark } = useData();
const posts = usePosts();
const today = formatDate(new Date(), "yyyy-MM-dd");
// 获取一年前的时间
const beforeOnYear = formatDate(
new Date(new Date().getTime() - 365 * 24 * 60 * 60 * 1000),
"yyyy-MM-dd"
);
const contributeList = computed(() => {
const contributeObject = ref({});
posts.value.sortPostsByDate.forEach((item) => {
const date = item.date.substring(0, 10);
if (contributeObject.value[date]) contributeObject.value[date]++;
else contributeObject.value[date] = 1;
});
const contributeDays = Object.keys(contributeObject.value);
return contributeDays
.map((item: string) => [item, contributeObject.value[item]])
.reverse();
});
const chartRef = useTemplateRef("chartRef");
const contributeChart = ref();
const option = {
tooltip: {
formatter: function (params) {
return `${params.value[0]} <br/> ${params.value[1]} 篇文章`;
},
},
visualMap: {
show: false,
min: 0,
max: 5,
inRange: {
color: ["#ebedf0", "#c6e48b", "#7bc96f", "#239a3b", "#196127", "#196127"],
},
},
calendar: {
left: "center",
itemStyle: {
color: "#ebedf0",
borderWidth: 5,
borderColor: "#fff",
shadowBlur: 0,
},
cellSize: [20, 20],
range: [beforeOnYear, today],
splitLine: true,
dayLabel: {
firstDay: 7,
nameMap: "ZH",
color: "#3c3c43",
},
monthLabel: {
color: "#3c3c43",
},
yearLabel: {
show: true,
position: "right",
},
silent: {
show: false,
},
},
series: {
type: "heatmap",
coordinateSystem: "calendar",
data: [],
},
};
const renderChart = (data: any) => {
option.calendar.itemStyle.borderColor = isDark.value ? "#1b1b1f" : "#fff";
option.calendar.itemStyle.color = isDark.value ? "#787878" : "#ebedf0";
if (contributeChart.value) echarts.dispose(contributeChart.value);
if (chartRef.value)
contributeChart.value = echarts.init(chartRef.value as HTMLElement);
option.series.data = data;
contributeChart.value.setOption(option);
};
watch(
contributeList,
async (newValue) => {
await nextTick();
renderChart(newValue);
},
{ immediate: true }
);
watch(isDark, () => {
renderChart(contributeList.value);
});
</script>
<template>
<div class="contribute__chart">
<div class="chart__box" ref="chartRef"></div>
</div>
</template>
<style>
.tk-page.tk-archives {
max-width: 1220px;
}
.tk-archives .contribute__chart {
width: 100%;
height: 260px;
}
.tk-archives .contribute__chart .chart__box {
margin: auto;
width: 100%;
height: 100%;
}
</style>
注册组件
在docs\.vitepress\theme\components\TeekLayoutProvider.vue
中注册ContributeChart
组件
vue
<script setup lang="ts" name="TeekLayoutProvider">
// @ts-ignore
import ContributeChart from "./ContributeChart.vue"; //导入贡献图组件
</script>
<template>
//其他组件...
<template #teek-archives-top-before>
<!-- 贡献图组件 -->
<ContributeChart />
</template>
</Teek.Layout>
</template>