├── screenshots ├── 1.png └── 2.png ├── fonts └── Hubot-Sans.woff2 ├── .gitignore ├── images └── placeholder-avatar.svg ├── LICENSE ├── README.md ├── debug.js ├── monthly_activity.js ├── index.html ├── heatmap.js ├── yearly_contributions.js └── styles.css /screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IICSC/github-contributions/HEAD/screenshots/1.png -------------------------------------------------------------------------------- /screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IICSC/github-contributions/HEAD/screenshots/2.png -------------------------------------------------------------------------------- /fonts/Hubot-Sans.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IICSC/github-contributions/HEAD/fonts/Hubot-Sans.woff2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | workspace.xml 2 | junitvmwatcher*.properties 3 | build.properties 4 | 5 | # generated java classes and java source files 6 | # manually add any custom artifacts that can't be generated from the models 7 | # http://confluence.jetbrains.com/display/MPSD25/HowTo+--+MPS+and+Git 8 | classes_gen 9 | source_gen 10 | source_gen.caches 11 | 12 | # generated test code and test results 13 | test_gen 14 | test_gen.caches 15 | TEST-*.xml 16 | junit*.properties 17 | -------------------------------------------------------------------------------- /images/placeholder-avatar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 IICSC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub 贡献统计 2 | 3 | 一个优雅的 GitHub 贡献数据可视化工具,展示用户的贡献热力图、年度统计和连续贡献等数据。 4 | 5 | ![预览截图1](./screenshots/1.png) 6 | 7 | ![预览截图2](./screenshots/2.png) 8 | 9 | **🌐 体验最新Commit:https://iicsc.damon233.top/github-contributions** 10 | 11 | ## ✨ 特性 12 | 13 | - 📊 贡献热力图展示 14 | - 📈 年度贡献统计图表 15 | - 🔥 连续贡献天数统计 16 | - 🎯 总贡献数据统计 17 | - 💫 流畅的动画效果 18 | - 🌓 简洁现代的界面设计 19 | 20 | ## 🚀 快速开始 21 | 22 | 1. 克隆仓库: 23 | ```bash 24 | git clone https://github.com/IICSC/github-contributions.git 25 | cd github-contributions 26 | ``` 27 | 28 | 2. 使用 HTTP 服务器运行项目(例如使用 Python): 29 | ```bash 30 | # Python 3 31 | python -m http.server 8000 32 | 33 | # Python 2 34 | python -m SimpleHTTPServer 8000 35 | ``` 36 | 37 | _**注:**`8000` **为端口,可以修改**_ 38 | 39 | 3. 在浏览器中访问:`http://localhost:8000` 40 | 41 | _**注:您也可以直接打开** `index.html`_ 42 | 43 | ## 🛠️ 项目结构 44 | 45 | ``` 46 | github-contributions/ 47 | ├── fonts/ 48 | │ └── Hubot-Sans.woff2 # 自定义字体 49 | │ 50 | ├── images/ 51 | │ └── placeholder-avatar.svg # 头像占位 52 | │ 53 | ├── index.html # 主页面HTML结构 54 | │ ├── 导航栏 55 | │ ├── 用户输入区 56 | │ ├── 统计卡片 57 | │ ├── 图表区域 58 | │ └── 页脚 59 | │ 60 | ├── styles.css # 样式文件 61 | │ ├── 主题变量 62 | │ ├── 布局样式 63 | │ ├── 组件样式 64 | │ └── 动画效果 65 | │ 66 | ├── yearly_contributions.js # 年度统计相关 67 | │ ├── 数据处理 68 | │ ├── 图表生成 69 | │ ├── 动画效果 70 | │ └── 事件处理 71 | │ 72 | ├── heatmap.js # 贡献热力图相关 73 | │ ├── 热力图生成 74 | │ ├── 日期处理 75 | │ ├── 动画效果 76 | │ └── 交互处理 77 | │ 78 | └── monthly_activity.js # 月度活动统计 79 | ├── 数据处理 80 | └── 图表生成 81 | ``` 82 | 83 | ### 核心文件说明 84 | 85 | #### 📄 index.html 86 | - 定义页面结构和布局 87 | - 引入所需的样式和脚本 88 | - 包含主要的HTML组件 89 | 90 | #### 📄 styles.css 91 | - 定义自定义WOFF字体 92 | - 定义全局主题变量 93 | - 设置响应式布局 94 | - 实现组件样式 95 | - 定义动画效果 96 | 97 | #### 📄 yearly_contributions.js 98 | - 处理年度贡献数据 99 | - 生成年度统计图表 100 | - 计算连续贡献天数 101 | - 处理数据刷新逻辑 102 | 103 | #### 📄 heatmap.js 104 | - 生成贡献热力图 105 | - 处理日期和贡献数据 106 | - 实现交互效果 107 | - 管理动画效果 108 | 109 | #### 📄 monthly_activity.js 110 | - 处理月度活动数据 111 | - 生成月度统计图表 112 | 113 | ### 修改指南 114 | 115 | 1. 修改主题样式 116 | - 在 `styles.css` 中更改主题变量 117 | - 调整组件样式和动画效果 118 | 119 | 2. 更新页面结构 120 | - 在 `index.html` 中修改HTML结构 121 | - 添加或移除组件 122 | 123 | 3. 调整数据处理 124 | - 在对应的 JS 文件中修改数据处理逻辑 125 | - 更新图表配置和动画效果 126 | 127 | 4. 添加新功能 128 | - 创建新的 JS 文件 129 | - 在 `index.html` 中引入 130 | - 添加相应的样式和交互 131 | 132 | ## ⚖️ 许可证 133 | 134 | 本项目使用 [MIT 许可证](https://github.com/IICSC/github-contributions/blob/main/LICENSE) 开源 135 | 136 | ### 使用到的开源项目及资源 137 | 138 | - [Chart.js](https://github.com/chartjs/Chart.js)(MIT 许可证) 139 | - [Hubot Sans](https://github.com/github/hubot-sans)(OFL-1.1 许可证) 140 | -------------------------------------------------------------------------------- /debug.js: -------------------------------------------------------------------------------- 1 | // 调试脚本 - 可通过浏览器控制台运行 2 | // 使用方法: 打开浏览器控制台,复制粘贴此脚本并执行 3 | 4 | (function() { 5 | console.log("=== GitHub贡献统计调试工具 ==="); 6 | console.log("浏览器时区信息:", Intl.DateTimeFormat().resolvedOptions().timeZone); 7 | console.log("时区偏移量(小时):", -new Date().getTimezoneOffset() / 60); 8 | 9 | // 测试日期 10 | const testDate = new Date(); 11 | console.log("当前日期:", testDate.toISOString()); 12 | console.log("本地日期格式:", testDate.toLocaleDateString()); 13 | 14 | // 测试API请求 15 | const testUsername = document.getElementById('username').value.trim() || 'WuXiaoMuer'; 16 | console.log("测试用户名:", testUsername); 17 | 18 | fetch(`https://github-contributions-api.jogruber.de/v4/${testUsername}`) 19 | .then(response => { 20 | console.log("API响应状态:", response.status, response.statusText); 21 | return response.json(); 22 | }) 23 | .then(data => { 24 | console.log("API返回数据:", data); 25 | 26 | if (data && data.contributions && data.contributions.length > 0) { 27 | const firstItem = data.contributions[0]; 28 | const lastItem = data.contributions[data.contributions.length - 1]; 29 | 30 | console.log("第一条贡献记录:", firstItem); 31 | console.log("最后一条贡献记录:", lastItem); 32 | 33 | // 检查日期范围 34 | const startDate = new Date(data.contributions[0].date); 35 | const endDate = new Date(data.contributions[data.contributions.length - 1].date); 36 | 37 | console.log("贡献日期范围:", startDate.toISOString(), "至", endDate.toISOString()); 38 | console.log("总贡献天数:", data.contributions.length); 39 | 40 | // 按年份统计 41 | const yearCounts = {}; 42 | data.contributions.forEach(item => { 43 | const year = new Date(item.date).getFullYear(); 44 | if (!yearCounts[year]) { 45 | yearCounts[year] = 0; 46 | } 47 | yearCounts[year]++; 48 | }); 49 | 50 | console.log("各年份贡献天数:", yearCounts); 51 | 52 | // 检查是否有无效日期 53 | const invalidDates = data.contributions.filter(item => 54 | !item.date || isNaN(new Date(item.date).getTime()) 55 | ); 56 | 57 | if (invalidDates.length > 0) { 58 | console.error("发现无效日期:", invalidDates); 59 | } else { 60 | console.log("未发现无效日期"); 61 | } 62 | 63 | // 显示热力图中的日期匹配情况 64 | if (typeof isLeapYear === 'function') { 65 | // 检查当前年份 66 | const currentYear = new Date().getFullYear(); 67 | console.log(`${currentYear}是${isLeapYear(currentYear) ? '闰年' : '平年'}`); 68 | 69 | // 检查某个特定年份的日期 70 | const targetYear = 2024; 71 | const daysInYear = isLeapYear(targetYear) ? 366 : 365; 72 | console.log(`${targetYear}年共${daysInYear}天`); 73 | 74 | // 检查API返回的2024年数据 75 | const year2024Data = data.contributions.filter(item => 76 | item.date.startsWith('2024-') 77 | ); 78 | 79 | console.log(`API返回的${targetYear}年数据:`, year2024Data.length, "条"); 80 | } 81 | } else { 82 | console.error("API返回的数据格式不正确或为空"); 83 | } 84 | }) 85 | .catch(error => { 86 | console.error("API请求失败:", error); 87 | }); 88 | 89 | // 检查DOM元素 90 | console.log("热力图容器:", document.getElementById('heatmap')); 91 | console.log("月度图表容器:", document.getElementById('monthlyChart')); 92 | console.log("年度图表容器:", document.getElementById('yearlyChart')); 93 | })(); -------------------------------------------------------------------------------- /monthly_activity.js: -------------------------------------------------------------------------------- 1 | const processMonthlyData = (contributions) => { 2 | const monthlyData = {}; 3 | 4 | // 添加调试信息 5 | console.log('开始处理月度数据,总条目数:', contributions?.length || 0); 6 | 7 | // 检查数据是否有效 8 | if (!contributions || !Array.isArray(contributions) || contributions.length === 0) { 9 | console.warn('月度数据为空或无效'); 10 | return {}; 11 | } 12 | 13 | // 获取当前日期,用于筛选未来日期 14 | const now = new Date(); 15 | const currentYear = now.getFullYear(); 16 | const currentMonth = now.getMonth() + 1; 17 | const currentDay = now.getDate(); 18 | const currentDateStr = `${currentYear}-${String(currentMonth).padStart(2, '0')}-${String(currentDay).padStart(2, '0')}`; 19 | 20 | console.log('月度图表当前日期:', currentDateStr); 21 | 22 | // 按日期排序 23 | const sortedContributions = [...contributions].sort((a, b) => a.date.localeCompare(b.date)); 24 | 25 | sortedContributions.forEach(day => { 26 | try { 27 | // 检查是否为未来日期 28 | if (day.date > currentDateStr) { 29 | // 跳过未来日期 30 | return; 31 | } 32 | 33 | // 检查日期有效性 34 | const dateObj = new Date(day.date); 35 | if (isNaN(dateObj.getTime())) { 36 | console.warn('月度图表忽略无效日期:', day); 37 | return; 38 | } 39 | 40 | // 提取年月 41 | const [yearStr, monthStr] = day.date.split('-'); 42 | const monthKey = `${yearStr}-${monthStr}`; 43 | 44 | if (!monthlyData[monthKey]) { 45 | monthlyData[monthKey] = 0; 46 | } 47 | monthlyData[monthKey] += day.count; 48 | } catch (e) { 49 | console.error('处理月度数据时出错:', e, day); 50 | } 51 | }); 52 | 53 | // 按时间顺序排序月份 54 | const sortedData = {}; 55 | Object.keys(monthlyData) 56 | .sort() 57 | .forEach(key => { 58 | sortedData[key] = monthlyData[key]; 59 | }); 60 | 61 | console.log('月度数据处理完成,月份数:', Object.keys(sortedData).length); 62 | return sortedData; 63 | }; 64 | 65 | document.addEventListener('DOMContentLoaded', () => { 66 | // monthly_activity.js中的图表将通过refreshData函数触发创建 67 | // 此处不需要直接初始化 68 | console.log('月度活动图表模块已加载'); 69 | }); 70 | 71 | window.createMonthlyChart = (data) => { 72 | if (!data || !data.contributions || !Array.isArray(data.contributions)) { 73 | console.error('月度图表创建失败: 无效数据格式', data); 74 | return; 75 | } 76 | 77 | console.log('开始创建月度图表'); 78 | const ctx = document.getElementById('monthlyChart')?.getContext('2d'); 79 | if (!ctx) { 80 | console.error('找不到月度图表canvas元素'); 81 | return; 82 | } 83 | 84 | try { 85 | const monthlyData = processMonthlyData(data.contributions); 86 | 87 | if (window.monthlyChart) { 88 | window.monthlyChart.destroy(); 89 | } 90 | 91 | const labels = Object.keys(monthlyData); 92 | const values = Object.values(monthlyData); 93 | 94 | if (labels.length === 0) { 95 | console.warn('没有月度数据可显示'); 96 | return; 97 | } 98 | 99 | // 格式化标签,将YYYY-MM转换为YYYY年MM月 100 | const formattedLabels = labels.map(label => { 101 | const [year, month] = label.split('-'); 102 | return `${year}年${parseInt(month)}月`; 103 | }); 104 | 105 | // 生成背景颜色渐变 106 | const backgroundColors = values.map((value, index) => { 107 | // 根据贡献值计算颜色深浅 108 | const alpha = Math.min(Math.max(value / 100, 0.1), 0.5); 109 | return `rgba(75, 192, 192, ${alpha})`; 110 | }); 111 | 112 | window.monthlyChart = new Chart(ctx, { 113 | type: 'line', 114 | data: { 115 | labels: formattedLabels, 116 | datasets: [{ 117 | label: '月度贡献', 118 | data: values, 119 | borderColor: 'rgb(75, 192, 192)', 120 | backgroundColor: backgroundColors, 121 | tension: 0.2, 122 | fill: true, 123 | pointBackgroundColor: 'rgb(75, 192, 192)', 124 | pointRadius: 4, 125 | pointHoverRadius: 6 126 | }] 127 | }, 128 | options: { 129 | responsive: true, 130 | animation: { 131 | duration: 2000, 132 | easing: 'easeOutQuart' 133 | }, 134 | scales: { 135 | y: { 136 | beginAtZero: true, 137 | grid: { 138 | display: false 139 | }, 140 | ticks: { 141 | precision: 0 142 | } 143 | }, 144 | x: { 145 | grid: { 146 | display: false 147 | } 148 | } 149 | }, 150 | plugins: { 151 | tooltip: { 152 | callbacks: { 153 | title: function(context) { 154 | return context[0].label; 155 | }, 156 | label: function(context) { 157 | return `贡献数: ${context.raw}`; 158 | } 159 | } 160 | } 161 | } 162 | } 163 | }); 164 | 165 | console.log('月度图表创建成功'); 166 | } catch (e) { 167 | console.error('创建月度图表时出错:', e); 168 | } 169 | }; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GitHub 贡献统计 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 26 |
27 |
28 |

GitHub 贡献统计

29 |
30 |
31 | 32 |
33 | User Avatar 35 | 36 | 37 |
38 |
39 | 43 |
44 |
45 | 46 |
47 |
48 |

总贡献

49 |
0
50 |
51 |
52 |

当前连续贡献

53 |
0
54 |
55 |
56 |

最长连续贡献

57 |
0
58 |
59 |
60 | 61 |
62 |
63 | 64 |
65 | 66 |
67 | 68 |
69 | 70 |
71 | 72 |
73 | 74 | 109 |
110 |
111 | 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /heatmap.js: -------------------------------------------------------------------------------- 1 | const createHeatmap = (data) => { 2 | const heatmapContainer = document.getElementById('heatmap'); 3 | if (!heatmapContainer) { 4 | console.error('找不到热力图容器'); 5 | return; 6 | } 7 | 8 | // 清空容器但保留已有内容的引用 9 | const oldHeader = heatmapContainer.querySelector('.heatmap-header'); 10 | const oldYearSelector = heatmapContainer.querySelector('.year-selector'); 11 | 12 | heatmapContainer.innerHTML = ''; 13 | 14 | // 创建标题和图标 15 | const titleContainer = document.createElement('div'); 16 | titleContainer.className = 'heatmap-header'; 17 | 18 | const icon = document.createElement('span'); 19 | icon.className = 'contribution-icon'; 20 | icon.innerHTML = ` 21 | 22 | `; 23 | 24 | const title = document.createElement('h3'); 25 | title.textContent = '贡献日历'; 26 | title.className = 'heatmap-title'; 27 | 28 | titleContainer.appendChild(icon); 29 | titleContainer.appendChild(title); 30 | heatmapContainer.appendChild(titleContainer); 31 | 32 | // 检查数据是否有效 33 | if (!data || !data.contributions || data.contributions.length === 0) { 34 | console.error('热力图数据无效或为空'); 35 | 36 | // 创建空的贡献日历 37 | const emptyCalendar = document.createElement('div'); 38 | emptyCalendar.className = 'contribution-calendar'; 39 | emptyCalendar.innerHTML = '
暂无贡献数据
'; 40 | heatmapContainer.appendChild(emptyCalendar); 41 | 42 | return; 43 | } 44 | 45 | console.log('开始创建热力图,数据点数量:', data.contributions.length); 46 | 47 | // 预处理所有数据,创建全局的日期贡献映射 48 | const allContributionsMap = new Map(); 49 | data.contributions.forEach(day => { 50 | try { 51 | const date = new Date(day.date); 52 | if (isNaN(date.getTime())) { 53 | console.warn('忽略无效日期:', day.date); 54 | return; 55 | } 56 | allContributionsMap.set(day.date, day.count); 57 | } catch (e) { 58 | console.error('处理贡献数据时出错:', e); 59 | } 60 | }); 61 | 62 | // 按年份分组数据 - 主要用于年份选择器 63 | const yearlyData = {}; 64 | data.contributions.forEach(day => { 65 | try { 66 | const date = new Date(day.date); 67 | if (isNaN(date.getTime())) { 68 | return; 69 | } 70 | 71 | const year = date.getFullYear(); 72 | if (!yearlyData[year]) { 73 | yearlyData[year] = []; 74 | } 75 | yearlyData[year].push(day); 76 | } catch (e) { 77 | console.error('处理贡献数据时出错:', e); 78 | } 79 | }); 80 | 81 | // 检查是否有有效的年份数据 82 | if (Object.keys(yearlyData).length === 0) { 83 | console.error('没有有效的年份数据'); 84 | 85 | const emptyCalendar = document.createElement('div'); 86 | emptyCalendar.className = 'contribution-calendar'; 87 | emptyCalendar.innerHTML = '
无法解析贡献数据
'; 88 | heatmapContainer.appendChild(emptyCalendar); 89 | 90 | return; 91 | } 92 | 93 | // 创建年份选择器 94 | const yearSelector = document.createElement('div'); 95 | yearSelector.className = 'year-selector'; 96 | 97 | const years = Object.keys(yearlyData).sort().reverse(); 98 | console.log('可用年份:', years); 99 | 100 | if (years.length === 0) { 101 | console.error('未找到任何年份数据'); 102 | return; 103 | } 104 | 105 | const latestYear = years[0]; 106 | 107 | years.forEach((year, index) => { 108 | const yearBtn = document.createElement('button'); 109 | yearBtn.textContent = year; 110 | yearBtn.className = 'year-btn'; 111 | yearBtn.style.animationDelay = `${index * 0.1}s`; 112 | 113 | if (year === latestYear) { 114 | yearBtn.classList.add('active'); 115 | } 116 | 117 | yearBtn.onclick = () => { 118 | document.querySelectorAll('.year-btn').forEach(btn => { 119 | btn.classList.remove('active'); 120 | }); 121 | yearBtn.classList.add('active'); 122 | showYearContributions(year, yearlyData[year], allContributionsMap); 123 | }; 124 | yearSelector.appendChild(yearBtn); 125 | }); 126 | heatmapContainer.appendChild(yearSelector); 127 | 128 | // 创建日历容器 129 | const calendarContainer = document.createElement('div'); 130 | calendarContainer.className = 'contribution-calendar'; 131 | heatmapContainer.appendChild(calendarContainer); 132 | 133 | // 添加图例 134 | addLegend(heatmapContainer); 135 | 136 | // 立即显示最新年份的数据 137 | try { 138 | showYearContributions(latestYear, yearlyData[latestYear], allContributionsMap); 139 | } catch (e) { 140 | console.error('显示年度贡献时出错:', e); 141 | calendarContainer.innerHTML = '
显示贡献数据时出错
'; 142 | } 143 | 144 | console.log('热力图创建完成'); 145 | }; 146 | 147 | const showYearContributions = (year, contributions, allContributionsMap) => { 148 | const calendarContainer = document.querySelector('.contribution-calendar'); 149 | calendarContainer.classList.add('fade-out'); 150 | 151 | setTimeout(() => { 152 | calendarContainer.innerHTML = ''; 153 | 154 | // 创建月份标签 155 | const monthLabels = document.createElement('div'); 156 | monthLabels.className = 'month-labels'; 157 | ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'].forEach((month, index) => { 158 | const monthLabel = document.createElement('div'); 159 | monthLabel.className = 'month-label'; 160 | monthLabel.textContent = month + '月'; 161 | monthLabel.style.left = `${index * 4.2 + 3}rem`; 162 | monthLabels.appendChild(monthLabel); 163 | }); 164 | calendarContainer.appendChild(monthLabels); 165 | 166 | // 创建周标签 167 | const weekLabels = document.createElement('div'); 168 | weekLabels.className = 'week-labels'; 169 | ['日', '一', '二', '三', '四', '五', '六'].forEach(label => { 170 | const weekLabel = document.createElement('div'); 171 | weekLabel.className = 'week-label'; 172 | weekLabel.textContent = label; 173 | weekLabels.appendChild(weekLabel); 174 | }); 175 | calendarContainer.appendChild(weekLabels); 176 | 177 | // 创建网格容器 178 | const gridContainer = document.createElement('div'); 179 | gridContainer.className = 'contribution-grid'; 180 | 181 | // 调试日期格式问题 182 | console.log('贡献数据第一项:', contributions[0]); 183 | console.log('处理的年份:', year); 184 | 185 | // 检查是否有未来日期 186 | const now = new Date(); 187 | const currentYear = now.getFullYear(); 188 | const currentMonth = now.getMonth() + 1; 189 | const currentDay = now.getDate(); 190 | const currentDateStr = `${currentYear}-${String(currentMonth).padStart(2, '0')}-${String(currentDay).padStart(2, '0')}`; 191 | 192 | console.log('当前日期:', currentDateStr); 193 | 194 | // 生成当年所有日期并构建日历 195 | const yearNum = parseInt(year); 196 | const isLeapYearVal = isLeapYear(yearNum); 197 | const daysInYear = isLeapYearVal ? 366 : 365; 198 | 199 | console.log(`${year}年天数:`, daysInYear, isLeapYearVal ? '(闰年)' : ''); 200 | 201 | // 获取该年第一天的星期 202 | const firstDayOfYear = new Date(yearNum, 0, 1); 203 | const firstDayWeekday = firstDayOfYear.getDay(); // 0 是星期日,1是星期一... 204 | 205 | console.log(`${year}年第一天是星期:`, firstDayWeekday); 206 | 207 | // 生成该年所有日期 208 | const allDates = []; 209 | 210 | // 添加上一年的日期以填充第一周 211 | for (let i = 0; i < firstDayWeekday; i++) { 212 | // 修正计算逻辑,确保日期正确 213 | const daysToSubtract = firstDayWeekday - i; 214 | const prevYearDate = new Date(yearNum, 0, 1); 215 | prevYearDate.setDate(prevYearDate.getDate() - daysToSubtract); 216 | allDates.push(prevYearDate); 217 | } 218 | 219 | // 添加当年所有日期 220 | for (let i = 0; i < daysInYear; i++) { 221 | const currentDate = new Date(yearNum, 0, 1); 222 | currentDate.setDate(currentDate.getDate() + i); 223 | allDates.push(currentDate); 224 | } 225 | 226 | // 如果需要,添加下一年的日期以填充最后一周 227 | const lastDayOfYear = new Date(yearNum, 11, 31); 228 | const lastDayWeekday = lastDayOfYear.getDay(); 229 | if (lastDayWeekday < 6) { 230 | for (let i = 1; i <= 6 - lastDayWeekday; i++) { 231 | const nextYearDate = new Date(yearNum, 11, 31); 232 | nextYearDate.setDate(nextYearDate.getDate() + i); 233 | allDates.push(nextYearDate); 234 | } 235 | } 236 | 237 | console.log('日历总日期数:', allDates.length); 238 | 239 | // 创建日历网格 240 | const weeksCount = Math.ceil(allDates.length / 7); 241 | console.log('日历总周数:', weeksCount); 242 | 243 | // 以7x53的网格显示 244 | const grid = Array(7).fill().map(() => Array(weeksCount).fill(null)); 245 | 246 | // 填充日期到网格 247 | allDates.forEach((date, index) => { 248 | const col = Math.floor(index / 7); 249 | const row = index % 7; 250 | grid[row][col] = date; 251 | }); 252 | 253 | // 创建日历单元格 254 | grid.forEach((row, rowIndex) => { 255 | row.forEach((date, colIndex) => { 256 | if (!date) return; 257 | 258 | const cell = document.createElement('div'); 259 | cell.className = 'contribution-cell'; 260 | 261 | // 格式化日期为YYYY-MM-DD,与API返回格式一致 262 | const formattedYear = date.getFullYear(); 263 | const formattedMonth = String(date.getMonth() + 1).padStart(2, '0'); 264 | const formattedDay = String(date.getDate()).padStart(2, '0'); 265 | const formattedDate = `${formattedYear}-${formattedMonth}-${formattedDay}`; 266 | 267 | // 检查是否为未来日期 268 | const isFutureDate = formattedDate > currentDateStr; 269 | 270 | // 获取贡献数 - 使用全局贡献映射,无论日期属于哪一年 271 | const count = isFutureDate ? 0 : (allContributionsMap.get(formattedDate) || 0); 272 | 273 | // 为调试添加日期属性 274 | cell.setAttribute('data-date', formattedDate); 275 | if (isFutureDate) { 276 | cell.setAttribute('data-future', 'true'); 277 | } 278 | 279 | // 设置贡献等级 280 | if (isFutureDate) { 281 | cell.classList.add('future-date'); 282 | cell.classList.add('level-0'); 283 | } else { 284 | if (count === 0) cell.classList.add('level-0'); 285 | else if (count <= 3) cell.classList.add('level-1'); 286 | else if (count <= 6) cell.classList.add('level-2'); 287 | else if (count <= 9) cell.classList.add('level-3'); 288 | else cell.classList.add('level-4'); 289 | } 290 | 291 | // 添加提示信息 292 | const displayDate = `${formattedYear}年${date.getMonth() + 1}月${date.getDate()}日`; 293 | cell.title = isFutureDate ? `${displayDate}: 未来日期` : `${displayDate}: ${count} 次贡献`; 294 | 295 | // 如果日期不在当前年份内,添加淡化效果 296 | if (formattedYear !== yearNum) { 297 | cell.classList.add('outside-month'); 298 | } 299 | 300 | // 添加动画延迟 301 | const delay = Math.min((rowIndex * weeksCount + colIndex) * 0.001, 0.3); 302 | cell.style.animationDelay = `${delay}s`; 303 | 304 | gridContainer.appendChild(cell); 305 | }); 306 | }); 307 | 308 | calendarContainer.appendChild(gridContainer); 309 | 310 | calendarContainer.classList.remove('fade-out'); 311 | calendarContainer.classList.add('fade-in'); 312 | 313 | setTimeout(() => { 314 | calendarContainer.classList.remove('fade-in'); 315 | }, 300); 316 | }, 300); 317 | }; 318 | 319 | // 辅助函数:判断是否为闰年 320 | function isLeapYear(year) { 321 | return ((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0); 322 | } 323 | 324 | const addLegend = (container) => { 325 | const legend = document.createElement('div'); 326 | legend.className = 'contribution-legend'; 327 | legend.innerHTML = ` 328 | 贡献度: 329 |
330 |
331 | 0 332 |
333 |
334 |
335 | 1-3 336 |
337 |
338 |
339 | 4-6 340 |
341 |
342 |
343 | 7-9 344 |
345 |
346 |
347 | 10+ 348 |
349 | `; 350 | container.appendChild(legend); 351 | }; 352 | 353 | // 假设我们从API获取数据后调用这个函数 354 | fetch('https://github-contributions-api.jogruber.de/v4/WuXiaoMuer') 355 | .then(response => response.json()) 356 | .then(data => createHeatmap(data)) 357 | .catch(error => console.error('Error:', error)); -------------------------------------------------------------------------------- /yearly_contributions.js: -------------------------------------------------------------------------------- 1 | let yearlyChart = null; 2 | 3 | const createYearlyChart = (data) => { 4 | // 获取当前日期,用于筛选未来日期 5 | const now = new Date(); 6 | const currentDateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; 7 | 8 | console.log('年度图表当前日期:', currentDateStr); 9 | 10 | // 过滤掉未来日期和无效日期 11 | const validContributions = data.contributions.filter(day => { 12 | // 检查是否为有效日期 13 | const dateObj = new Date(day.date); 14 | if (isNaN(dateObj.getTime())) { 15 | console.warn('年度图表忽略无效日期:', day); 16 | return false; 17 | } 18 | 19 | // 检查是否为未来日期 20 | return day.date <= currentDateStr; 21 | }); 22 | 23 | console.log('有效贡献数据条目数:', validContributions.length); 24 | 25 | // 计算总贡献(所有年份的总和) 26 | const totalContributions = validContributions.reduce((sum, day) => sum + day.count, 0); 27 | animateNumber('totalContributions', totalContributions); 28 | 29 | // 计算连续贡献 30 | calculateStreaks(validContributions); 31 | 32 | // 处理年度数据 33 | const yearlyData = {}; 34 | validContributions.forEach(day => { 35 | // 从日期字符串中提取年份 36 | const year = day.date.split('-')[0]; 37 | if (!yearlyData[year]) { 38 | yearlyData[year] = 0; 39 | } 40 | yearlyData[year] += day.count; 41 | }); 42 | 43 | const years = Object.keys(yearlyData).sort(); 44 | const contributions = years.map(year => yearlyData[year]); 45 | 46 | const ctx = document.getElementById('yearlyChart').getContext('2d'); 47 | 48 | if (yearlyChart) { 49 | yearlyChart.destroy(); 50 | } 51 | 52 | // 生成背景颜色渐变 53 | const backgroundColors = contributions.map(value => { 54 | // 根据贡献值计算颜色深浅 55 | const intensity = Math.min(Math.max(value / 500, 0.3), 0.9); 56 | return `rgba(46, 164, 79, ${intensity})`; 57 | }); 58 | 59 | yearlyChart = new Chart(ctx, { 60 | type: 'bar', 61 | data: { 62 | labels: years, 63 | datasets: [{ 64 | label: '年度贡献数', 65 | data: contributions, 66 | backgroundColor: backgroundColors, 67 | borderColor: 'rgba(46, 164, 79, 1)', 68 | borderWidth: 1, 69 | borderRadius: 8, 70 | barThickness: 40 71 | }] 72 | }, 73 | options: { 74 | responsive: true, 75 | animation: { 76 | duration: 2000, 77 | easing: 'easeOutQuart' 78 | }, 79 | scales: { 80 | y: { 81 | beginAtZero: true, 82 | grid: { 83 | display: false 84 | }, 85 | ticks: { 86 | precision: 0 87 | } 88 | }, 89 | x: { 90 | grid: { 91 | display: false 92 | } 93 | } 94 | }, 95 | plugins: { 96 | legend: { 97 | display: false 98 | }, 99 | tooltip: { 100 | callbacks: { 101 | title: function(context) { 102 | return context[0].label + '年'; 103 | }, 104 | label: function(context) { 105 | return `贡献数: ${context.raw}`; 106 | } 107 | } 108 | } 109 | } 110 | } 111 | }); 112 | }; 113 | 114 | // 计算连续贡献天数 115 | const calculateStreaks = (contributions) => { 116 | // 确保数据非空 117 | if (!contributions || contributions.length === 0) { 118 | console.warn('计算连续贡献时发现空数据'); 119 | animateNumber('currentStreak', 0); 120 | animateNumber('longestStreak', 0); 121 | return; 122 | } 123 | 124 | // 获取当前日期(不含时分秒) 125 | const today = new Date(); 126 | today.setHours(0, 0, 0, 0); 127 | 128 | // 确保按日期排序,从最近到最早 129 | const sortedContributions = [...contributions].sort((a, b) => 130 | new Date(b.date).getTime() - new Date(a.date).getTime() 131 | ); 132 | 133 | console.log('计算连续贡献,共有数据点:', sortedContributions.length); 134 | console.log('最近的贡献日期:', sortedContributions[0]?.date); 135 | 136 | // 创建日期到贡献的映射 137 | const contributionMap = new Map(); 138 | sortedContributions.forEach(item => { 139 | contributionMap.set(item.date, item.count); 140 | }); 141 | 142 | // 计算当前连续贡献 143 | let currentStreak = 0; 144 | let checkDate = new Date(today); 145 | 146 | // 检查最近一次贡献是否是今天或昨天 147 | const latestContribution = sortedContributions[0]; 148 | const latestDate = latestContribution ? new Date(latestContribution.date) : null; 149 | latestDate?.setHours(0, 0, 0, 0); 150 | 151 | const oneDayMs = 24 * 60 * 60 * 1000; 152 | const daysSinceLatest = latestDate ? Math.round((today - latestDate) / oneDayMs) : null; 153 | 154 | console.log('最新贡献距今天数:', daysSinceLatest); 155 | 156 | // 如果最近的贡献不是今天或昨天,当前连续贡献为0 157 | if (daysSinceLatest === null || daysSinceLatest > 1) { 158 | console.log('最新贡献不是今天或昨天,当前连续为0'); 159 | currentStreak = 0; 160 | } else { 161 | // 从最近的贡献日期开始往前检查 162 | let streakEndDate = new Date(latestDate); 163 | 164 | // 如果最新贡献是昨天而不是今天,那么从昨天开始检查 165 | if (daysSinceLatest === 1) { 166 | console.log('最新贡献是昨天'); 167 | streakEndDate = new Date(latestDate); 168 | } else { 169 | console.log('最新贡献是今天'); 170 | } 171 | 172 | let keepCounting = true; 173 | 174 | while (keepCounting) { 175 | // 格式化日期为YYYY-MM-DD (使用UTC避免时区问题) 176 | const year = streakEndDate.getFullYear(); 177 | const month = String(streakEndDate.getMonth() + 1).padStart(2, '0'); 178 | const day = String(streakEndDate.getDate()).padStart(2, '0'); 179 | const dateStr = `${year}-${month}-${day}`; 180 | 181 | const count = contributionMap.get(dateStr) || 0; 182 | 183 | if (count > 0) { 184 | currentStreak++; 185 | console.log(`${dateStr} 有贡献,当前连续: ${currentStreak}`); 186 | 187 | // 前一天 (使用setDate以正确处理月份和年份边界) 188 | streakEndDate.setDate(streakEndDate.getDate() - 1); 189 | } else { 190 | console.log(`${dateStr} 无贡献,当前连续中断`); 191 | keepCounting = false; 192 | } 193 | } 194 | } 195 | 196 | // 计算最长连续贡献 197 | let longestStreak = 0; 198 | let currentLength = 0; 199 | let previousDate = null; 200 | 201 | // 按日期排序,从最早到最近 202 | const chronologicalContributions = [...contributions].sort((a, b) => 203 | new Date(a.date).getTime() - new Date(b.date).getTime() 204 | ); 205 | 206 | for (let i = 0; i < chronologicalContributions.length; i++) { 207 | const day = chronologicalContributions[i]; 208 | const currentDate = new Date(day.date); 209 | currentDate.setHours(0, 0, 0, 0); 210 | 211 | // 检查是否是连续的日期 212 | if (previousDate) { 213 | const diffDays = Math.round((currentDate - previousDate) / oneDayMs); 214 | 215 | if (diffDays > 1) { 216 | // 连续中断,重置当前记录 217 | currentLength = 0; 218 | } 219 | } 220 | 221 | if (day.count > 0) { 222 | currentLength++; 223 | longestStreak = Math.max(longestStreak, currentLength); 224 | } else { 225 | currentLength = 0; 226 | } 227 | 228 | previousDate = currentDate; 229 | } 230 | 231 | // 确保最长连续贡献至少与当前连续贡献一样多 232 | longestStreak = Math.max(longestStreak, currentStreak); 233 | 234 | // 更新统计数据 235 | animateNumber('currentStreak', currentStreak); 236 | animateNumber('longestStreak', longestStreak); 237 | 238 | console.log(`当前连续贡献: ${currentStreak}天, 最长连续贡献: ${longestStreak}天`); 239 | }; 240 | 241 | // 数字动画函数 242 | const animateNumber = (elementId, target) => { 243 | const element = document.getElementById(elementId); 244 | const duration = 1500; 245 | const start = parseInt(element.textContent) || 0; 246 | const increment = (target - start) / (duration / 16); 247 | let current = start; 248 | 249 | const animate = () => { 250 | current += increment; 251 | if ((increment > 0 && current >= target) || 252 | (increment < 0 && current <= target)) { 253 | element.textContent = target; 254 | return; 255 | } 256 | element.textContent = Math.round(current); 257 | requestAnimationFrame(animate); 258 | }; 259 | 260 | animate(); 261 | }; 262 | 263 | // 重置统计数据 264 | const resetStats = () => { 265 | // 清空所有统计数据 266 | ['totalContributions', 'currentStreak', 'longestStreak'].forEach(id => { 267 | const element = document.getElementById(id); 268 | if (element) { 269 | element.style.animation = 'none'; 270 | element.offsetHeight; // 触发重排 271 | element.style.animation = null; 272 | element.textContent = '0'; 273 | } 274 | }); 275 | 276 | // 清空图表 277 | if (yearlyChart) { 278 | yearlyChart.destroy(); 279 | yearlyChart = null; 280 | } 281 | 282 | // 清空月度图表 283 | if (window.monthlyChart) { 284 | window.monthlyChart.destroy(); 285 | window.monthlyChart = null; 286 | } 287 | 288 | console.log('所有统计数据已重置'); 289 | }; 290 | 291 | // 刷新数据的函数 292 | const refreshData = () => { 293 | const username = document.getElementById('username').value.trim(); 294 | if (!username) { 295 | showError('请输入用户名'); 296 | return; 297 | } 298 | 299 | // 更新用户头像 300 | const userAvatar = document.getElementById('userAvatar'); 301 | userAvatar.src = `https://github.com/${username}.png`; 302 | userAvatar.style.animation = 'none'; 303 | userAvatar.offsetHeight; // 触发重排 304 | userAvatar.style.animation = 'scaleIn 0.3s ease'; 305 | 306 | const refreshBtn = document.getElementById('refreshBtn'); 307 | const spinner = refreshBtn.querySelector('.loading-spinner'); 308 | const buttonText = refreshBtn.querySelector('.button-text'); 309 | 310 | refreshBtn.disabled = true; 311 | spinner.style.display = 'block'; 312 | buttonText.textContent = '加载中...'; 313 | 314 | // 先重置统计数字,但不清空图表 315 | ['totalContributions', 'currentStreak', 'longestStreak'].forEach(id => { 316 | const element = document.getElementById(id); 317 | if (element) { 318 | element.textContent = '0'; 319 | } 320 | }); 321 | 322 | // 添加调试信息 323 | console.log('开始请求GitHub贡献数据:', username); 324 | 325 | // 更新用户访问计数 326 | const userCounter = document.querySelector('.user-counter .counter-image'); 327 | if (userCounter) { 328 | userCounter.src = `https://profile-counter.glitch.me/${username}/count.svg`; 329 | } 330 | 331 | fetch(`https://github-contributions-api.jogruber.de/v4/${username}`) 332 | .then(response => { 333 | if (!response.ok) { 334 | console.error('API响应错误:', response.status, response.statusText); 335 | throw new Error(`API响应错误: ${response.status}`); 336 | } 337 | return response.json(); 338 | }) 339 | .then(data => { 340 | console.log('已获得数据:', data); 341 | 342 | // 检查数据格式是否正确 343 | if (!data || !data.contributions || !Array.isArray(data.contributions) || data.contributions.length === 0) { 344 | console.error('数据格式错误或为空:', data); 345 | throw new Error('获取的数据格式不正确或为空'); 346 | } 347 | 348 | // 检查是否有invalid date 349 | if (data.contributions.some(item => !item.date || isNaN(new Date(item.date).getTime()))) { 350 | console.error('数据中包含无效日期'); 351 | // 修复日期格式 352 | data.contributions = data.contributions.filter(item => { 353 | if (!item.date || isNaN(new Date(item.date).getTime())) { 354 | console.warn('跳过无效日期:', item); 355 | return false; 356 | } 357 | return true; 358 | }); 359 | 360 | if (data.contributions.length === 0) { 361 | throw new Error('过滤无效日期后没有可用数据'); 362 | } 363 | } 364 | 365 | // 更新图表和热力图 366 | createYearlyChart(data); 367 | createHeatmap(data); 368 | 369 | // 调用月度图表创建函数 370 | if (window.createMonthlyChart) { 371 | window.createMonthlyChart(data); 372 | } 373 | }) 374 | .catch(error => { 375 | console.error('Error:', error); 376 | showError('获取数据时出错: ' + (error.message || '请检查用户名并稍后重试')); 377 | resetStats(); 378 | }) 379 | .finally(() => { 380 | refreshBtn.disabled = false; 381 | spinner.style.display = 'none'; 382 | buttonText.textContent = '刷新数据'; 383 | }); 384 | }; 385 | 386 | // 修改错误提示 387 | const showError = (message) => { 388 | const errorDiv = document.createElement('div'); 389 | errorDiv.className = 'error-message'; 390 | errorDiv.innerHTML = ` 391 | 392 | 393 | 394 | ${message} 395 | `; 396 | 397 | const existingError = document.querySelector('.error-message'); 398 | if (existingError) { 399 | existingError.remove(); 400 | } 401 | 402 | const inputSection = document.querySelector('.input-section'); 403 | inputSection.appendChild(errorDiv); 404 | 405 | setTimeout(() => { 406 | errorDiv.classList.add('fade-out'); 407 | setTimeout(() => errorDiv.remove(), 300); 408 | }, 3000); 409 | }; 410 | 411 | // 添加事件监听 412 | document.addEventListener('DOMContentLoaded', () => { 413 | // 刷新按钮点击事件 414 | const refreshBtn = document.getElementById('refreshBtn'); 415 | if (refreshBtn) { 416 | refreshBtn.addEventListener('click', refreshData); 417 | } 418 | 419 | // 用户名输入框回车键提交 420 | const usernameInput = document.getElementById('username'); 421 | if (usernameInput) { 422 | usernameInput.addEventListener('keypress', (event) => { 423 | if (event.key === 'Enter') { 424 | event.preventDefault(); 425 | refreshData(); 426 | } 427 | }); 428 | } 429 | 430 | // 页面加载时获取默认用户数据 431 | setTimeout(() => { 432 | refreshData(); 433 | }, 100); 434 | 435 | console.log('事件监听器已设置'); 436 | }); 437 | 438 | // 添加导航栏自动隐藏 439 | let lastScrollY = window.scrollY; 440 | const navbar = document.querySelector('.navbar'); 441 | 442 | window.addEventListener('scroll', () => { 443 | const currentScrollY = window.scrollY; 444 | 445 | if (currentScrollY > lastScrollY) { 446 | navbar?.classList.add('hidden'); 447 | } else { 448 | navbar?.classList.remove('hidden'); 449 | } 450 | 451 | lastScrollY = currentScrollY; 452 | }); -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Hubot Sans'; 3 | src: 4 | url('./fonts/Hubot-Sans.woff2') format('woff2 supports variations'), 5 | url('./fonts/Hubot-Sans.woff2') format('woff2-variations'); 6 | font-weight: 200 500; 7 | font-stretch: 400% 125%; 8 | } 9 | 10 | html { 11 | font-family: 'Hubot Sans'; 12 | } 13 | 14 | :root { 15 | --primary-color: #2ea44f; 16 | --primary-hover: #2c974b; 17 | --bg-color: #f6f8fa; 18 | --card-bg: #ffffff; 19 | --text-primary: #24292e; 20 | --text-secondary: #586069; 21 | --border-color: #e1e4e8; 22 | --animation-duration: 0.6s; 23 | } 24 | 25 | * { 26 | margin: 0; 27 | padding: 0; 28 | box-sizing: border-box; 29 | } 30 | 31 | body { 32 | font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; 33 | background-color: var(--bg-color); 34 | color: var(--text-primary); 35 | line-height: 1.5; 36 | } 37 | 38 | .container { 39 | max-width: 1200px; 40 | margin: 0 auto; 41 | padding: 2rem; 42 | padding-top: 1rem; 43 | margin-top: 80px; 44 | } 45 | 46 | .header { 47 | text-align: center; 48 | margin-bottom: 2rem; 49 | font-family: 'Hubot Sans'; 50 | } 51 | 52 | .header h1 { 53 | font-size: 2.5rem; 54 | margin-bottom: 1.5rem; 55 | background: linear-gradient(120deg, #2ea44f, #238636); 56 | -webkit-background-clip: text; 57 | background-clip: text; 58 | -webkit-text-fill-color: transparent; 59 | animation: fadeInDown var(--animation-duration) ease; 60 | } 61 | 62 | .input-section { 63 | display: flex; 64 | gap: 1rem; 65 | justify-content: center; 66 | margin-bottom: 2rem; 67 | animation: fadeInUp var(--animation-duration) ease; 68 | } 69 | 70 | /* 输入框组样式 */ 71 | .input-group { 72 | position: relative; 73 | flex: 1; 74 | max-width: 300px; 75 | } 76 | 77 | .input-group label { 78 | display: block; 79 | color: var(--text-secondary); 80 | font-size: 0.875rem; 81 | margin-bottom: 0.5rem; 82 | } 83 | 84 | .input-wrapper { 85 | position: relative; 86 | display: flex; 87 | align-items: center; 88 | gap: 0.75rem; 89 | } 90 | 91 | .user-avatar { 92 | width: 48px; 93 | height: 48px; 94 | border-radius: 50%; 95 | border: 2px solid var(--primary-color); 96 | transition: all 0.3s ease; 97 | animation: scaleIn 0.3s ease; 98 | } 99 | 100 | #username { 101 | flex: 1; 102 | padding: 0.75rem 1rem; 103 | font-size: 1rem; 104 | background: transparent; 105 | border: none; 106 | border-bottom: 2px solid var(--border-color); 107 | transition: all 0.3s ease; 108 | } 109 | 110 | #username:hover { 111 | border-bottom-color: var(--text-secondary); 112 | } 113 | 114 | #username:focus { 115 | outline: none; 116 | border-bottom-color: var(--primary-color); 117 | } 118 | 119 | /* 头像动画 */ 120 | @keyframes scaleIn { 121 | from { 122 | transform: scale(0.8); 123 | opacity: 0; 124 | } 125 | 126 | to { 127 | transform: scale(1); 128 | opacity: 1; 129 | } 130 | } 131 | 132 | /* 年份选择器样式 */ 133 | .year-selector { 134 | display: flex; 135 | gap: 0.5rem; 136 | margin-bottom: 1.5rem; 137 | padding: 0.5rem; 138 | background: var(--bg-color); 139 | border-radius: 8px; 140 | overflow-x: auto; 141 | -webkit-overflow-scrolling: touch; 142 | } 143 | 144 | .year-btn { 145 | padding: 0.5rem 1.25rem; 146 | border: none; 147 | background: transparent; 148 | color: var(--text-secondary); 149 | font-size: 0.9rem; 150 | font-weight: 500; 151 | cursor: pointer; 152 | border-radius: 6px; 153 | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 154 | position: relative; 155 | overflow: hidden; 156 | } 157 | 158 | .year-btn::before { 159 | content: ''; 160 | position: absolute; 161 | top: 0; 162 | left: 0; 163 | width: 100%; 164 | height: 100%; 165 | background: var(--primary-color); 166 | border-radius: 6px; 167 | transform: scaleX(0); 168 | transform-origin: left; 169 | transition: transform 0.3s ease; 170 | z-index: -1; 171 | } 172 | 173 | .year-btn:hover { 174 | color: var(--primary-color); 175 | } 176 | 177 | .year-btn.active { 178 | color: white; 179 | transform: scale(1.05); 180 | } 181 | 182 | .year-btn.active::before { 183 | transform: scaleX(1); 184 | } 185 | 186 | /* 刷新按钮样式优化 */ 187 | #refreshBtn { 188 | padding: 0.75rem 1.5rem; 189 | background: linear-gradient(45deg, var(--primary-color), var(--primary-hover)); 190 | color: white; 191 | border: none; 192 | border-radius: 8px; 193 | font-size: 1rem; 194 | cursor: pointer; 195 | transition: all 0.3s ease; 196 | display: flex; 197 | align-items: center; 198 | gap: 0.5rem; 199 | position: relative; 200 | overflow: hidden; 201 | box-shadow: 0 2px 4px rgba(46, 164, 79, 0.2); 202 | } 203 | 204 | #refreshBtn:hover { 205 | transform: translateY(-2px); 206 | box-shadow: 0 4px 8px rgba(46, 164, 79, 0.3); 207 | } 208 | 209 | #refreshBtn:active { 210 | transform: translateY(0); 211 | box-shadow: 0 2px 4px rgba(46, 164, 79, 0.2); 212 | } 213 | 214 | .loading-spinner { 215 | display: none; 216 | width: 16px; 217 | height: 16px; 218 | border: 2px solid #ffffff; 219 | border-top: 2px solid transparent; 220 | border-radius: 50%; 221 | animation: spin 1s linear infinite; 222 | } 223 | 224 | .stats-container { 225 | display: grid; 226 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 227 | gap: 1.5rem; 228 | margin-bottom: 2rem; 229 | font-family: 'Hubot Sans'; 230 | } 231 | 232 | .stats-card { 233 | background: var(--card-bg); 234 | padding: 1.5rem; 235 | border-radius: 12px; 236 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); 237 | text-align: center; 238 | animation: fadeInUp var(--animation-duration) ease; 239 | transition: transform 0.3s ease; 240 | position: relative; 241 | overflow: hidden; 242 | } 243 | 244 | .stats-card::before { 245 | content: ''; 246 | position: absolute; 247 | top: 0; 248 | left: 0; 249 | width: 100%; 250 | height: 4px; 251 | background: linear-gradient(90deg, var(--primary-color), var(--primary-hover)); 252 | opacity: 0; 253 | transition: opacity 0.3s ease; 254 | } 255 | 256 | .stats-card:hover::before { 257 | opacity: 1; 258 | } 259 | 260 | .stats-card h3 { 261 | color: var(--text-secondary); 262 | font-size: 1rem; 263 | margin-bottom: 0.5rem; 264 | } 265 | 266 | .stats-card .number { 267 | font-size: 2.5rem; 268 | font-weight: 600; 269 | color: var(--primary-color); 270 | background: linear-gradient(120deg, var(--primary-color), var(--primary-hover)); 271 | -webkit-background-clip: text; 272 | background-clip: text; 273 | -webkit-text-fill-color: transparent; 274 | transition: all 0.3s ease; 275 | } 276 | 277 | .chart-card { 278 | background: var(--card-bg); 279 | padding: 1.5rem; 280 | border-radius: 12px; 281 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); 282 | margin-bottom: 1.5rem; 283 | animation: fadeInUp var(--animation-duration) ease; 284 | transition: all 0.3s ease; 285 | } 286 | 287 | .chart-card:hover { 288 | transform: translateY(-2px); 289 | box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1); 290 | } 291 | 292 | /* 热力图样式 */ 293 | .contribution-calendar { 294 | position: relative; 295 | padding: 3.5rem 2.5rem 2rem; 296 | background: var(--card-bg); 297 | border-radius: 12px; 298 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); 299 | overflow: hidden; 300 | } 301 | 302 | .month-labels { 303 | position: absolute; 304 | top: 1.2rem; 305 | left: 5rem; 306 | right: 1rem; 307 | height: 20px; 308 | } 309 | 310 | .month-label { 311 | position: absolute; 312 | font-size: 0.85rem; 313 | color: var(--text-secondary); 314 | transform-origin: left; 315 | white-space: nowrap; 316 | } 317 | 318 | .week-labels { 319 | position: absolute; 320 | left: 1.2rem; 321 | top: 3.6rem; 322 | display: flex; 323 | flex-direction: column; 324 | gap: 0; 325 | height: calc(7 * 14px + 6 * 2px); 326 | justify-content: space-between; 327 | } 328 | 329 | .week-label { 330 | font-size: 0.8rem; 331 | color: var(--text-secondary); 332 | text-align: center; 333 | width: 1.5rem; 334 | height: 14px; 335 | line-height: 14px; 336 | opacity: 0.8; 337 | } 338 | 339 | .contribution-grid { 340 | margin-left: 3rem; 341 | display: grid; 342 | grid-template-columns: repeat(53, 14px); 343 | grid-template-rows: repeat(7, 14px); 344 | gap: 2px; 345 | } 346 | 347 | .contribution-cell { 348 | width: 14px; 349 | height: 14px; 350 | border-radius: 2px; 351 | transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); 352 | animation: scaleIn 0.15s ease forwards; 353 | transform: scale(0); 354 | opacity: 0; 355 | cursor: pointer; 356 | } 357 | 358 | .contribution-cell:hover { 359 | transform: scale(1.2); 360 | } 361 | 362 | .contribution-cell.outside-month { 363 | opacity: 0.7; 364 | border: 1px solid rgba(0, 0, 0, 0.03); 365 | } 366 | 367 | @keyframes scaleIn { 368 | from { 369 | transform: scale(0); 370 | opacity: 0; 371 | } 372 | 373 | to { 374 | transform: scale(1); 375 | opacity: 1; 376 | } 377 | } 378 | 379 | /* 贡献等级颜色 */ 380 | .level-0 { 381 | background-color: #ebedf0; 382 | } 383 | 384 | .level-1 { 385 | background-color: #9be9a8; 386 | } 387 | 388 | .level-2 { 389 | background-color: #40c463; 390 | } 391 | 392 | .level-3 { 393 | background-color: #30a14e; 394 | } 395 | 396 | .level-4 { 397 | background-color: #216e39; 398 | } 399 | 400 | @keyframes fadeIn { 401 | from { 402 | opacity: 0; 403 | } 404 | 405 | to { 406 | opacity: 1; 407 | } 408 | } 409 | 410 | /* 动画关键帧 */ 411 | @keyframes fadeInDown { 412 | from { 413 | opacity: 0; 414 | transform: translateY(-20px); 415 | } 416 | 417 | to { 418 | opacity: 1; 419 | transform: translateY(0); 420 | } 421 | } 422 | 423 | @keyframes fadeInUp { 424 | from { 425 | opacity: 0; 426 | transform: translateY(20px); 427 | } 428 | 429 | to { 430 | opacity: 1; 431 | transform: translateY(0); 432 | } 433 | } 434 | 435 | @keyframes spin { 436 | to { 437 | transform: rotate(360deg); 438 | } 439 | } 440 | 441 | /* 响应式设计 */ 442 | @media (max-width: 768px) { 443 | .container { 444 | padding: 1rem; 445 | } 446 | 447 | .header h1 { 448 | font-size: 2rem; 449 | } 450 | 451 | .input-section { 452 | flex-direction: column; 453 | } 454 | 455 | #username { 456 | width: 100%; 457 | } 458 | 459 | .navbar { 460 | padding: 1rem; 461 | flex-direction: column; 462 | gap: 1rem; 463 | } 464 | 465 | .nav-links { 466 | gap: 1rem; 467 | } 468 | 469 | .contribution-calendar { 470 | padding: 3rem 1rem 1.5rem; 471 | } 472 | 473 | .week-labels { 474 | left: 0.5rem; 475 | } 476 | 477 | .contribution-grid { 478 | margin-left: 2rem; 479 | } 480 | 481 | .month-labels { 482 | left: 2rem; 483 | } 484 | } 485 | 486 | /* 图例 */ 487 | .contribution-legend { 488 | display: flex; 489 | align-items: center; 490 | padding-top: 1rem; 491 | margin-top: 1rem; 492 | border-top: 1px solid var(--border-color); 493 | justify-content: center; 494 | gap: 0.75rem; 495 | color: var(--text-secondary); 496 | font-size: 0.875rem; 497 | } 498 | 499 | .legend-item { 500 | display: flex; 501 | align-items: center; 502 | padding: 4px 8px; 503 | border-radius: 4px; 504 | transition: all 0.2s ease; 505 | gap: 4px; 506 | } 507 | 508 | .legend-color { 509 | width: 12px; 510 | height: 12px; 511 | border-radius: 2px; 512 | } 513 | 514 | /* 更新热力图容器样式 */ 515 | #heatmap { 516 | margin: 20px 0; 517 | background: var(--card-bg); 518 | border-radius: 12px; 519 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); 520 | padding: 1.5rem; 521 | overflow-x: auto; 522 | } 523 | 524 | .heatmap-title { 525 | font-size: 1.2rem; 526 | color: var(--text-primary); 527 | margin: 0; 528 | line-height: 1.4; 529 | } 530 | 531 | .heatmap-header { 532 | display: flex; 533 | align-items: center; 534 | gap: 0.5rem; 535 | margin-bottom: 1rem; 536 | } 537 | 538 | .contribution-icon { 539 | color: var(--primary-color); 540 | display: flex; 541 | align-items: center; 542 | animation: rotate-in 0.6s ease; 543 | margin-top: 2px; 544 | /* 微调图标位置 */ 545 | } 546 | 547 | /* 新增动画关键帧 */ 548 | @keyframes rotate-in { 549 | from { 550 | transform: rotate(-180deg); 551 | opacity: 0; 552 | } 553 | 554 | to { 555 | transform: rotate(0); 556 | opacity: 1; 557 | } 558 | } 559 | 560 | @keyframes slide-in-right { 561 | from { 562 | transform: translateX(20px); 563 | opacity: 0; 564 | } 565 | 566 | to { 567 | transform: translateX(0); 568 | opacity: 1; 569 | } 570 | } 571 | 572 | @keyframes pulse { 573 | 0% { 574 | transform: scale(1); 575 | } 576 | 577 | 50% { 578 | transform: scale(1.05); 579 | } 580 | 581 | 100% { 582 | transform: scale(1); 583 | } 584 | } 585 | 586 | .fade-out { 587 | animation: fadeOut 0.3s ease forwards; 588 | } 589 | 590 | @keyframes fadeOut { 591 | from { 592 | opacity: 1; 593 | transform: translateY(0); 594 | } 595 | 596 | to { 597 | opacity: 0; 598 | transform: translateY(-10px); 599 | } 600 | } 601 | 602 | /* 错误消息样式 */ 603 | .error-message { 604 | display: flex; 605 | align-items: center; 606 | justify-content: center; 607 | gap: 0.5rem; 608 | color: #cf222e; 609 | background-color: #ffebe9; 610 | border: 1px solid #ff818266; 611 | border-radius: 6px; 612 | padding: 0.75rem; 613 | margin-top: 0.5rem; 614 | font-size: 0.875rem; 615 | width: 100%; 616 | animation: slideInDown 0.3s ease; 617 | } 618 | 619 | .error-icon { 620 | fill: currentColor; 621 | } 622 | 623 | /* 加载状态样式 */ 624 | #refreshBtn:disabled { 625 | opacity: 0.7; 626 | cursor: not-allowed; 627 | } 628 | 629 | /* 新增动画 */ 630 | @keyframes slideInDown { 631 | from { 632 | transform: translateY(-10px); 633 | opacity: 0; 634 | } 635 | 636 | to { 637 | transform: translateY(0); 638 | opacity: 1; 639 | } 640 | } 641 | 642 | /* 页脚样式 */ 643 | .footer { 644 | margin-top: 2rem; 645 | padding: 2rem; 646 | border-top: 1px solid var(--border-color); 647 | display: flex; 648 | flex-direction: column; 649 | align-items: center; 650 | gap: 1rem; 651 | text-align: center; 652 | } 653 | 654 | .author { 655 | display: flex; 656 | align-items: center; 657 | } 658 | 659 | .author-link { 660 | display: flex; 661 | align-items: center; 662 | gap: 0.75rem; 663 | text-decoration: none; 664 | color: var(--text-primary); 665 | font-weight: 500; 666 | transition: opacity 0.2s ease; 667 | margin-right: 10px; 668 | } 669 | 670 | .author-link:hover { 671 | opacity: 0.8; 672 | } 673 | 674 | .author-avatar { 675 | width: 40px; 676 | height: 40px; 677 | border-radius: 50%; 678 | border: 2px solid var(--border-color); 679 | } 680 | 681 | .repo-info { 682 | display: flex; 683 | flex-direction: column; 684 | align-items: center; 685 | gap: 0.5rem; 686 | } 687 | 688 | .repo-link { 689 | display: flex; 690 | align-items: center; 691 | gap: 0.5rem; 692 | text-decoration: none; 693 | color: var(--text-secondary); 694 | font-size: 0.875rem; 695 | transition: color 0.2s ease; 696 | } 697 | 698 | .repo-link:hover { 699 | color: var(--primary-color); 700 | } 701 | 702 | .license { 703 | font-size: 0.75rem; 704 | color: var(--text-secondary); 705 | } 706 | 707 | .license a { 708 | color: inherit; 709 | text-decoration: none; 710 | } 711 | 712 | .license a:hover { 713 | text-decoration: underline; 714 | } 715 | 716 | /* 导航栏样式 */ 717 | .navbar { 718 | background: var(--card-bg); 719 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 720 | padding: 1rem 2rem; 721 | position: fixed; 722 | top: 0; 723 | left: 0; 724 | right: 0; 725 | z-index: 100; 726 | display: flex; 727 | justify-content: space-between; 728 | align-items: center; 729 | transform: translateY(0); 730 | transition: transform 0.3s ease; 731 | } 732 | 733 | .navbar.hidden { 734 | transform: translateY(-100%); 735 | } 736 | 737 | .nav-brand { 738 | display: flex; 739 | align-items: center; 740 | gap: 1rem; 741 | } 742 | 743 | .nav-avatar { 744 | width: 32px; 745 | height: 32px; 746 | border-radius: 50%; 747 | border: 2px solid var(--primary-color); 748 | } 749 | 750 | .nav-title { 751 | font-size: 1.25rem; 752 | font-weight: 600; 753 | color: var(--text-primary); 754 | background: linear-gradient(120deg, var(--primary-color), var(--primary-hover)); 755 | -webkit-background-clip: text; 756 | background-clip: text; 757 | -webkit-text-fill-color: transparent; 758 | font-family: 'Hubot Sans'; 759 | } 760 | 761 | .nav-links { 762 | display: flex; 763 | gap: 2rem; 764 | } 765 | 766 | .nav-link { 767 | text-decoration: none; 768 | color: var(--text-secondary); 769 | font-size: 0.95rem; 770 | font-weight: 500; 771 | padding: 0.5rem 0; 772 | position: relative; 773 | transition: color 0.3s ease; 774 | } 775 | 776 | .nav-link::after { 777 | content: ''; 778 | position: absolute; 779 | bottom: 0; 780 | left: 0; 781 | width: 100%; 782 | height: 2px; 783 | background: var(--primary-color); 784 | transform: scaleX(0); 785 | transform-origin: right; 786 | transition: transform 0.3s ease; 787 | } 788 | 789 | .nav-link:hover { 790 | color: var(--primary-color); 791 | } 792 | 793 | .nav-link:hover::after { 794 | transform: scaleX(1); 795 | transform-origin: left; 796 | } 797 | 798 | .nav-link.active { 799 | color: var(--primary-color); 800 | } 801 | 802 | /* 添加GitHub图标容器样式 */ 803 | .github-icon-wrapper { 804 | display: flex; 805 | align-items: center; 806 | color: var(--text-secondary); 807 | margin-right: 8px; 808 | order: -1; 809 | } 810 | 811 | /* 访问量统计容器样式 */ 812 | .visit-counter { 813 | margin-top: 0; 814 | margin-bottom: 2rem; 815 | text-align: center; 816 | display: flex; 817 | flex-direction: row; 818 | justify-content: center; 819 | align-items: center; 820 | gap: 3rem; 821 | padding: 1.5rem; 822 | background: var(--card-bg); 823 | border-radius: 12px; 824 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); 825 | } 826 | 827 | .counter-label { 828 | color: var(--text-secondary); 829 | font-size: 0.9rem; 830 | margin-bottom: 0.5rem; 831 | } 832 | 833 | .counter-image { 834 | height: 24px; 835 | border-radius: 4px; 836 | } 837 | 838 | .user-counter { 839 | margin-top: 0; 840 | } 841 | 842 | /* 添加计数器项容器样式 */ 843 | .visit-counter > div { 844 | display: flex; 845 | flex-direction: column; 846 | align-items: center; 847 | min-width: 200px; 848 | } 849 | 850 | /* 热力图贡献等级 */ 851 | .contribution-cell.level-0 { 852 | background-color: #ebedf0; 853 | } 854 | 855 | .contribution-cell.level-1 { 856 | background-color: #9be9a8; 857 | } 858 | 859 | .contribution-cell.level-2 { 860 | background-color: #40c463; 861 | } 862 | 863 | .contribution-cell.level-3 { 864 | background-color: #30a14e; 865 | } 866 | 867 | .contribution-cell.level-4 { 868 | background-color: #216e39; 869 | } 870 | 871 | /* 未来日期样式 */ 872 | .contribution-cell.future-date { 873 | background-color: #f0f0f0; 874 | border: 1px dashed #ccc; 875 | opacity: 0.7; 876 | } 877 | 878 | .dark-mode .contribution-cell.future-date { 879 | background-color: #2d333b; 880 | border: 1px dashed #444c56; 881 | } 882 | 883 | /* 无数据时的提示样式 */ 884 | .empty-message { 885 | display: flex; 886 | align-items: center; 887 | justify-content: center; 888 | height: 200px; 889 | color: var(--text-secondary); 890 | font-size: 1.1rem; 891 | text-align: center; 892 | background-color: rgba(0, 0, 0, 0.02); 893 | border-radius: 8px; 894 | border: 1px dashed var(--border-color); 895 | margin: 1rem 0; 896 | } 897 | 898 | .fade-in { 899 | animation: fadeIn 0.3s ease forwards; 900 | } --------------------------------------------------------------------------------