Created
December 20, 2025 00:01
-
-
Save goldengrape/9d18e457381086d11f370f28859722d4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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