Skip to content

Instantly share code, notes, and snippets.

@goldengrape
Created December 20, 2025 00:01
Show Gist options
  • Select an option

  • Save goldengrape/9d18e457381086d11f370f28859722d4 to your computer and use it in GitHub Desktop.

Select an option

Save goldengrape/9d18e457381086d11f370f28859722d4 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GitHub 风格文件日志生成器 (自定义配色版)</title>
<!-- 引入 html2canvas 库 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<style>
:root {
/* 默认颜色变量,稍后会通过 JS 动态覆盖 */
--bg-color: #0F172A; /* 默认深蓝色背景 */
--text-color: #E2E8F0;
--border-color: #334155;
/* 瓷砖颜色等级 */
--level-0: #1E293B; /* 空数据 - 稍亮的蓝 */
--level-1: #0e4429;
--level-2: #006d32;
--level-3: #26a641;
--level-4: #39d353;
}
* { box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
transition: background-color 0.3s ease;
}
h1 { margin-bottom: 20px; font-size: 24px; text-align: center;}
/* 设置面板 */
.panel {
background: rgba(255, 255, 255, 0.05);
padding: 20px;
border-radius: 8px;
border: 1px solid var(--border-color);
margin-bottom: 20px;
width: 100%;
max-width: 900px;
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: space-between;
}
.control-group {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 150px;
}
.control-group label {
font-size: 14px;
font-weight: 600;
color: #94a3b8;
}
.color-pickers {
display: flex;
gap: 5px;
}
input[type="color"] {
border: none;
width: 32px;
height: 32px;
cursor: pointer;
background: none;
border-radius: 4px;
}
input[type="number"] {
padding: 6px;
border-radius: 4px;
border: 1px solid #475569;
background: #0f172a;
color: white;
width: 80px;
}
input[type="file"] {
padding: 5px;
color: var(--text-color);
}
.btn-primary {
background-color: #2563eb;
color: white;
border: 1px solid rgba(255,255,255,0.1);
padding: 8px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: background-color 0.2s;
align-self: flex-end;
}
.btn-primary:hover { background-color: #1d4ed8; }
.btn-primary:active { transform: translateY(1px); }
/* 图表容器 */
#render-target {
width: 100%;
max-width: 1000px; /* 稍微宽一点以容纳更多内容 */
padding: 40px;
background-color: var(--bg-color);
border-radius: 8px;
/* 为了导出好看,默认边框去掉,只留背景 */
}
.year-block {
margin-bottom: 30px;
display: flex;
align-items: flex-start;
gap: 15px;
}
.year-label {
font-size: 14px;
font-weight: bold;
width: 40px;
padding-top: 2px;
}
.graph {
display: inline-grid;
grid-template-rows: repeat(7, 12px); /* 7行: 周日-周六 */
grid-auto-flow: column;
gap: 4px;
}
.day {
width: 12px;
height: 12px;
border-radius: 2px;
background-color: var(--level-0);
position: relative;
}
/* 应用颜色变量 */
.day[data-level="0"] { background-color: var(--level-0); }
.day[data-level="1"] { background-color: var(--level-1); }
.day[data-level="2"] { background-color: var(--level-2); }
.day[data-level="3"] { background-color: var(--level-3); }
.day[data-level="4"] { background-color: var(--level-4); }
/* Tooltip */
.day:hover::after {
content: attr(data-title);
position: absolute;
bottom: 120%;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.8);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
z-index: 100;
pointer-events: none;
}
</style>
</head>
<body>
<h1>文件日志可视化 (PNG生成器)</h1>
<!-- 控制面板 -->
<div class="panel">
<!-- 文件上传 -->
<div class="control-group" style="flex: 1; min-width: 250px;">
<label>1. 上传文件列表 (txt/md)</label>
<input type="file" id="fileInput" accept=".txt,.md,.csv">
</div>
<!-- 颜色设置 -->
<div class="control-group">
<label>2. 配色方案 (背景 / 0-4级)</label>
<div class="color-pickers">
<input type="color" id="colorBg" value="#0F172A" title="背景色">
<span style="border-right: 1px solid #444; margin: 0 5px;"></span>
<input type="color" id="colorL0" value="#1E293B" title="无数据 (Level 0)">
<input type="color" id="colorL1" value="#0e4429" title="Level 1">
<input type="color" id="colorL2" value="#006d32" title="Level 2">
<input type="color" id="colorL3" value="#26a641" title="Level 3">
<input type="color" id="colorL4" value="#39d353" title="Level 4 (High)">
</div>
</div>
<!-- 导出设置 -->
<div class="control-group">
<label>3. 图片设置 (DPI)</label>
<div style="display: flex; gap: 10px; align-items: center;">
<input type="number" id="dpiInput" value="150" min="72" max="600" step="1">
<button class="btn-primary" id="downloadBtn" onclick="downloadImage()">下载 PNG</button>
</div>
</div>
</div>
<!-- 渲染区域 -->
<div id="render-target">
<div id="placeholder-text" style="color: #64748b; text-align: center; padding: 50px;">
请上传文件以生成图表...
</div>
</div>
<script>
// === 全局变量 ===
let fileData = {};
// === 初始化颜色监听 ===
const colorInputs = {
'--bg-color': 'colorBg',
'--level-0': 'colorL0',
'--level-1': 'colorL1',
'--level-2': 'colorL2',
'--level-3': 'colorL3',
'--level-4': 'colorL4'
};
// 绑定颜色改变事件
Object.entries(colorInputs).forEach(([cssVar, elementId]) => {
const el = document.getElementById(elementId);
el.addEventListener('input', (e) => {
document.documentElement.style.setProperty(cssVar, e.target.value);
});
// 初始化设置一次
document.documentElement.style.setProperty(cssVar, el.value);
});
// === 文件处理 ===
document.getElementById('fileInput').addEventListener('change', handleFileSelect, false);
function handleFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
processData(e.target.result);
};
reader.readAsText(file);
}
function processData(text) {
const lines = text.split(/\r?\n/);
fileData = {};
lines.forEach(line => {
line = line.trim();
if (!line) return;
// 正则解析 YYYYMMDD
const match = line.match(/^(\d{4})(\d{2})(\d{2})/);
if (match) {
const year = match[1];
const dateStr = `${year}-${match[2]}-${match[3]}`;
if (!fileData[year]) fileData[year] = {};
if (!fileData[year][dateStr]) fileData[year][dateStr] = 0;
fileData[year][dateStr]++;
}
});
renderCharts(fileData);
}
function renderCharts(data) {
const container = document.getElementById('render-target');
container.innerHTML = ''; // 清空
const years = Object.keys(data).sort((a, b) => b - a);
if (years.length === 0) {
container.innerHTML = '<div style="text-align:center;color:#888;">未找到匹配的文件名格式 (YYYYMMDD-...)</div>';
return;
}
years.forEach(year => {
const yearBlock = document.createElement('div');
yearBlock.className = 'year-block';
const yearLabel = document.createElement('div');
yearLabel.className = 'year-label';
yearLabel.textContent = year;
yearBlock.appendChild(yearLabel);
const graph = document.createElement('div');
graph.className = 'graph';
generateYearGrid(year, data[year], graph);
yearBlock.appendChild(graph);
container.appendChild(yearBlock);
});
}
function generateYearGrid(year, yearData, graphElement) {
const startDate = new Date(`${year}-01-01`);
const endDate = new Date(`${year}-12-31`);
// 0=周日, ... 6=周六
const startDayOfWeek = startDate.getDay();
// 填充前面的空白,GitHub 第一列是周日
for (let i = 0; i < startDayOfWeek; i++) {
const empty = document.createElement('div');
empty.className = 'day';
empty.style.visibility = 'hidden';
graphElement.appendChild(empty);
}
let curr = new Date(startDate);
while (curr <= endDate) {
const y = curr.getFullYear();
const m = String(curr.getMonth() + 1).padStart(2, '0');
const d = String(curr.getDate()).padStart(2, '0');
const isoDate = `${y}-${m}-${d}`;
const count = yearData[isoDate] || 0;
const dayEl = document.createElement('div');
dayEl.className = 'day';
dayEl.dataset.level = getLevel(count);
dayEl.dataset.title = `${isoDate}: ${count} items`;
graphElement.appendChild(dayEl);
curr.setDate(curr.getDate() + 1);
}
}
function getLevel(count) {
if (count === 0) return 0;
if (count <= 1) return 1;
if (count <= 2) return 2;
if (count <= 4) return 3;
return 4;
}
// === 下载功能 (核心修改) ===
function downloadImage() {
const element = document.getElementById('render-target');
const downloadBtn = document.getElementById('downloadBtn');
const dpi = parseInt(document.getElementById('dpiInput').value) || 150;
// 检查是否有内容
if (!element.querySelector('.year-block')) {
alert('请先上传文件生成图表!');
return;
}
downloadBtn.textContent = '生成中...';
downloadBtn.disabled = true;
// 计算缩放比例
// 标准屏幕一般被认为是 96dpi. 如果要 150dpi,scale = 150 / 96
const scale = dpi / 96;
// 获取当前的背景色,确保截图包含背景
const currentBg = getComputedStyle(document.documentElement).getPropertyValue('--bg-color').trim();
html2canvas(element, {
scale: scale, // 关键:控制分辨率
backgroundColor: currentBg, // 关键:强制背景色
logging: false,
useCORS: true
}).then(canvas => {
// 创建下载链接
const link = document.createElement('a');
link.download = `contribution-graph-${dpi}dpi.png`;
link.href = canvas.toDataURL("image/png");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
downloadBtn.textContent = '下载 PNG';
downloadBtn.disabled = false;
}).catch(err => {
console.error("导出失败:", err);
alert("导出图片失败,请检查控制台错误。");
downloadBtn.textContent = '下载 PNG';
downloadBtn.disabled = false;
});
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment