162 lines
9.8 KiB
Python
162 lines
9.8 KiB
Python
#!/usr/bin/env python3
|
||
# Generates the git history UI JavaScript and appends to app.js
|
||
# Using chr() for quote characters to avoid shell/heredoc quoting issues
|
||
|
||
sq = chr(39) # single quote
|
||
dq = chr(34) # double quote
|
||
nl = chr(10) # newline
|
||
bs = chr(92) # backslash
|
||
|
||
lines = [
|
||
'',
|
||
'// --- Git 版本历史 UI ---',
|
||
'',
|
||
'/** 打开 Git 版本历史面板 */',
|
||
'async function showGitHistory() {',
|
||
' if (!_currentProjectId) return;',
|
||
' const overlay = document.createElement(' + sq + 'div' + sq + ');',
|
||
' overlay.className = ' + sq + 'modal-overlay' + sq + ';',
|
||
' overlay.id = ' + sq + 'gitHistoryOverlay' + sq + ';',
|
||
' overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };',
|
||
' overlay.innerHTML = `',
|
||
' <div class="modal-content" style="max-width:600px;max-height:90vh;overflow-y:auto">',
|
||
' <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">',
|
||
' <h3 style="margin:0;font-size:16px;color:var(--text-1)">版本历史</h3>',
|
||
' <button class="btn btn-ghost" style="padding:2px 8px;font-size:18px"',
|
||
' onclick="document.getElementById(' + sq + 'gitHistoryOverlay' + sq + ').remove()">×</button>',
|
||
' </div>',
|
||
' <div id="gitLogList" style="display:flex;flex-direction:column;gap:8px">',
|
||
' <p style="color:var(--text-3);font-size:13px;text-align:center;padding:20px">加载中...</p>',
|
||
' </div>',
|
||
' <div id="gitDiffArea" style="display:none;margin-top:14px;background:var(--bg-2);border-radius:8px;padding:14px">',
|
||
' <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">',
|
||
' <span id="gitDiffTitle" style="font-size:12px;font-weight:600;color:var(--text-2);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></span>',
|
||
' <button class="btn btn-danger" id="gitRevertBtn" style="font-size:11px;padding:4px 10px;margin-left:10px;flex-shrink:0"',
|
||
' onclick="confirmRevert()">回滚到此版本</button>',
|
||
' </div>',
|
||
' <pre id="gitDiffContent" style="font-family:monospace;font-size:11px;line-height:1.6;',
|
||
' white-space:pre-wrap;word-break:break-all;max-height:360px;overflow-y:auto;',
|
||
' background:var(--bg-3);padding:10px;border-radius:6px;margin:0;color:var(--text-1)"></pre>',
|
||
' </div>',
|
||
' </div>`;',
|
||
' document.body.appendChild(overlay);',
|
||
' await _loadGitLog();',
|
||
'}',
|
||
'',
|
||
'// 当前选中的 commit hash(用于回滚)',
|
||
'let _selectedCommitHash = null;',
|
||
'',
|
||
'/** 加载提交历史列表 */',
|
||
'async function _loadGitLog() {',
|
||
' const listEl = document.getElementById(' + sq + 'gitLogList' + sq + ');',
|
||
' if (!listEl) return;',
|
||
' const { status, data } = await api(' + sq + 'GET' + sq + ', ' + sq + '/v1/projects/git/log?id=' + sq + ' + _currentProjectId + ' + sq + '&limit=30' + sq + ');',
|
||
' if (status !== 200 || !data.commits) {',
|
||
' listEl.innerHTML = ' + sq + '<p style="color:var(--error);font-size:13px;text-align:center;padding:20px">加载失败</p>' + sq + ';',
|
||
' return;',
|
||
' }',
|
||
' const commits = data.commits;',
|
||
' if (commits.length === 0) {',
|
||
' listEl.innerHTML = ' + sq + '<p style="color:var(--text-3);font-size:13px;text-align:center;padding:20px">暂无提交记录</p>' + sq + ';',
|
||
' return;',
|
||
' }',
|
||
' listEl.innerHTML = commits.map((c, idx) => `',
|
||
' <div class="git-commit-item" data-hash="${c.hash}"',
|
||
' onclick="_showCommitDiff(this.dataset.hash, this.dataset.msg)" data-msg="${_escHtml(c.message)}"',
|
||
' style="display:flex;align-items:flex-start;gap:12px;padding:10px 12px;background:var(--bg-2);',
|
||
' border-radius:8px;cursor:pointer;transition:background .15s;border:1px solid transparent"',
|
||
' onmouseover="this.style.borderColor=\'var(--border-d)\'" onmouseout="this.style.borderColor=\'transparent\'">',
|
||
' <div style="flex-shrink:0;width:8px;height:8px;border-radius:50%;background:var(--primary);margin-top:5px"></div>',
|
||
' <div style="flex:1;min-width:0">',
|
||
' <div style="font-size:13px;color:var(--text-1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${_escHtml(c.message)}</div>',
|
||
' <div style="font-size:11px;color:var(--text-3);margin-top:2px">',
|
||
' <code style="background:var(--bg-3);padding:1px 5px;border-radius:3px;font-size:10px">${c.hash}</code>',
|
||
' ${_fmtDate(c.date)} · ${_escHtml(c.author)}',
|
||
' </div>',
|
||
' </div>',
|
||
' ${idx === 0 ? ' + sq + '<span style="font-size:10px;color:var(--primary);background:rgba(99,102,241,.12);padding:1px 6px;border-radius:10px;flex-shrink:0;margin-top:2px">最新</span>' + sq + ' : ' + sq + sq + '}',
|
||
' </div>`).join(' + sq + sq + ');',
|
||
'}',
|
||
'',
|
||
'/** 展示某次提交的 diff */',
|
||
'async function _showCommitDiff(hash, message) {',
|
||
' _selectedCommitHash = hash;',
|
||
' const diffArea = document.getElementById(' + sq + 'gitDiffArea' + sq + ');',
|
||
' const diffContent = document.getElementById(' + sq + 'gitDiffContent' + sq + ');',
|
||
' const diffTitle = document.getElementById(' + sq + 'gitDiffTitle' + sq + ');',
|
||
' if (!diffArea) return;',
|
||
' diffTitle.textContent = hash + ' + sq + ' ' + sq + ' + (message || ' + sq + sq + ');',
|
||
' diffContent.textContent = ' + sq + '加载中...' + sq + ';',
|
||
' diffArea.style.display = ' + sq + 'block' + sq + ';',
|
||
' // 高亮选中行',
|
||
' document.querySelectorAll(' + sq + '.git-commit-item' + sq + ').forEach(el => {',
|
||
' el.style.background = el.dataset.hash === hash ? ' + sq + 'var(--bg-3)' + sq + ' : ' + sq + 'var(--bg-2)' + sq + ';',
|
||
' });',
|
||
' const { status, data } = await api(' + sq + 'GET' + sq + ', ' + sq + '/v1/projects/git/diff?id=' + sq + ' + _currentProjectId + ' + sq + '&hash=' + sq + ' + encodeURIComponent(hash));',
|
||
' if (status !== 200) { diffContent.textContent = ' + sq + '获取 diff 失败' + sq + '; return; }',
|
||
' const rawText = (data.stat || ' + sq + sq + ') + ' + sq + bs + 'n' + bs + 'n' + sq + ' + (data.diff || ' + sq + '(无变更)' + sq + ');',
|
||
' // 简单着色: + / -',
|
||
' diffContent.innerHTML = rawText',
|
||
' .split(' + sq + bs + 'n' + sq + ')',
|
||
' .map(ln => {',
|
||
' const safe = _escHtml(ln);',
|
||
' if (ln.startsWith(' + sq + '+++' + sq + ') || ln.startsWith(' + sq + '---' + sq + ')) return ' + sq + '<span style="color:var(--text-2)">' + sq + ' + safe + ' + sq + '</span>' + sq + ';',
|
||
' if (ln.startsWith(' + sq + '+' + sq + ')) return ' + sq + '<span style="color:#4ade80">' + sq + ' + safe + ' + sq + '</span>' + sq + ';',
|
||
' if (ln.startsWith(' + sq + '-' + sq + ')) return ' + sq + '<span style="color:#f87171">' + sq + ' + safe + ' + sq + '</span>' + sq + ';',
|
||
' return safe;',
|
||
' })',
|
||
' .join(' + sq + bs + 'n' + sq + ');',
|
||
'}',
|
||
'',
|
||
'/** 确认并执行回滚 */',
|
||
'async function confirmRevert() {',
|
||
' if (!_selectedCommitHash || !_currentProjectId) return;',
|
||
' const h = _selectedCommitHash;',
|
||
' if (!confirm(' + sq + '确定回滚到版本 ' + sq + ' + h + ' + sq + ' 吗?当前未保存的变更将被覆盖。此操作产生新提交,不破坏历史记录。' + sq + ')) return;',
|
||
' const btn = document.getElementById(' + sq + 'gitRevertBtn' + sq + ');',
|
||
' if (btn) { btn.disabled = true; btn.textContent = ' + sq + '回滚中...' + sq + '; }',
|
||
' const { status, data } = await api(' + sq + 'POST' + sq + ', ' + sq + '/v1/projects/git/revert' + sq + ', { projectId: _currentProjectId, hash: h });',
|
||
' if (btn) { btn.disabled = false; btn.textContent = ' + sq + '回滚到此版本' + sq + '; }',
|
||
' if (status === 200) {',
|
||
' toast(' + sq + '已回滚到版本 ' + sq + ' + h, ' + sq + 'success' + sq + ');',
|
||
' document.getElementById(' + sq + 'gitHistoryOverlay' + sq + ')?.remove();',
|
||
' if (typeof openProject === ' + sq + 'function' + sq + ') openProject(_currentProjectId);',
|
||
' } else {',
|
||
' toast((data && data.error) || ' + sq + '回滚失败' + sq + ', ' + sq + 'error' + sq + ');',
|
||
' }',
|
||
'}',
|
||
'',
|
||
'/** HTML 转义辅助 */',
|
||
'function _escHtml(s) {',
|
||
' return String(s || ' + sq + sq + ')',
|
||
' .replace(/&/g, ' + sq + '&' + sq + ')',
|
||
' .replace(/</g, ' + sq + '<' + sq + ')',
|
||
' .replace(/>/g, ' + sq + '>' + sq + ')',
|
||
' .replace(/"/g, ' + sq + '"' + sq + ');',
|
||
'}',
|
||
'',
|
||
'/** 格式化 ISO 日期为简短可读格式 */',
|
||
'function _fmtDate(iso) {',
|
||
' try {',
|
||
' const d = new Date(iso);',
|
||
' const now = new Date();',
|
||
' const diff = (now - d) / 1000;',
|
||
' if (diff < 60) return ' + sq + '刚刚' + sq + ';',
|
||
' if (diff < 3600) return Math.floor(diff / 60) + ' + sq + ' 分钟前' + sq + ';',
|
||
' if (diff < 86400) return Math.floor(diff / 3600) + ' + sq + ' 小时前' + sq + ';',
|
||
' if (diff < 2592000) return Math.floor(diff / 86400) + ' + sq + ' 天前' + sq + ';',
|
||
' return d.toLocaleDateString(' + sq + 'zh-CN' + sq + ', { year: ' + sq + 'numeric' + sq + ', month: ' + sq + '2-digit' + sq + ', day: ' + sq + '2-digit' + sq + ' });',
|
||
' } catch(e) { return iso || ' + sq + sq + '; }',
|
||
'}',
|
||
'',
|
||
]
|
||
|
||
content = nl.join(lines) + nl
|
||
|
||
import tempfile, os
|
||
out_path = os.path.join(tempfile.gettempdir(), 'git_ui_append.js')
|
||
with open(out_path, 'w', encoding='utf-8') as f:
|
||
f.write(content)
|
||
|
||
print('Generated %s (%d bytes, %d lines)' % (out_path, len(content), content.count(nl)))
|