Skip to content

Commit 84d0b5f

Browse files
author
hongyu9
committed
支持导出git diff
1 parent 8f2561d commit 84d0b5f

File tree

5 files changed

+297
-12
lines changed

5 files changed

+297
-12
lines changed

contentScript.js

Lines changed: 204 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,18 @@
2626
// 立即注入样式,保证按钮和弹窗都美观
2727
injectStyle();
2828

29-
if (window.__gitlab_export_btn_injected) return;
30-
window.__gitlab_export_btn_injected = true;
29+
if (window.__gitlab_export_btn_injected && window.__gitlab_copy_diff_btn_injected) return;
30+
31+
// 检查是否是merge request页面
32+
const isMergeRequestPage = window.location.href.includes('/merge_requests/');
33+
34+
if (!window.__gitlab_export_btn_injected) {
35+
window.__gitlab_export_btn_injected = true;
36+
}
37+
38+
if (isMergeRequestPage && !window.__gitlab_copy_diff_btn_injected) {
39+
window.__gitlab_copy_diff_btn_injected = true;
40+
}
3141

3242
// 插入导出按钮
3343
function insertExportBtn() {
@@ -414,7 +424,197 @@
414424
return '';
415425
}
416426

417-
const observer = new MutationObserver(insertExportBtn);
427+
// 插入复制git diff按钮
428+
function insertCopyDiffBtn() {
429+
// 检查是否是merge request页面
430+
if (!window.location.href.includes('/merge_requests/')) return;
431+
432+
// 如果按钮已存在,则不重复添加
433+
if (document.getElementById('gitlab-copy-diff-btn')) return;
434+
435+
// 查找Edit按钮
436+
const editBtn = document.querySelector('.detail-page-header-actions a.js-issuable-edit');
437+
if (!editBtn) return;
438+
439+
// 创建复制git diff按钮
440+
const btn = document.createElement('button');
441+
btn.id = 'gitlab-copy-diff-btn';
442+
btn.textContent = '复制git diff';
443+
btn.className = 'gl-button btn btn-md btn-default gl-display-none gl-md-display-block';
444+
btn.style.marginRight = '8px';
445+
btn.onclick = copyGitDiff;
446+
447+
// 创建按钮内部的span元素,与Edit按钮结构保持一致
448+
const spanText = document.createElement('span');
449+
spanText.className = 'gl-button-text';
450+
spanText.textContent = '复制git diff';
451+
btn.textContent = ''; // 清空按钮文本
452+
btn.appendChild(spanText);
453+
454+
// 插入按钮到Edit按钮前面
455+
editBtn.parentNode.insertBefore(btn, editBtn);
456+
457+
// 同时在移动端下拉菜单中添加复制git diff选项
458+
try {
459+
const mobileEditItem = document.querySelector('[data-testid="edit-merge-request"]');
460+
if (mobileEditItem) {
461+
const mobileMenu = mobileEditItem.parentNode;
462+
463+
const copyDiffItem = document.createElement('li');
464+
copyDiffItem.className = 'gl-new-dropdown-item';
465+
copyDiffItem.tabIndex = 0;
466+
copyDiffItem.dataset.testid = 'copy-git-diff';
467+
468+
const copyDiffButton = document.createElement('button');
469+
copyDiffButton.tabIndex = -1;
470+
copyDiffButton.type = 'button';
471+
copyDiffButton.className = 'gl-new-dropdown-item-content';
472+
copyDiffButton.onclick = copyGitDiff;
473+
474+
const textWrapper = document.createElement('span');
475+
textWrapper.className = 'gl-new-dropdown-item-text-wrapper';
476+
textWrapper.textContent = '复制git diff';
477+
478+
copyDiffButton.appendChild(textWrapper);
479+
copyDiffItem.appendChild(copyDiffButton);
480+
481+
// 插入到Edit选项前面
482+
mobileMenu.insertBefore(copyDiffItem, mobileEditItem);
483+
}
484+
} catch (e) {
485+
console.error('添加移动端复制git diff选项失败:', e);
486+
}
487+
}
488+
489+
// 复制git diff功能
490+
function copyGitDiff(event) {
491+
// 阻止事件冒泡,防止触发其他事件
492+
if (event) {
493+
event.preventDefault();
494+
event.stopPropagation();
495+
}
496+
497+
// 获取当前页面URL
498+
const currentUrl = window.location.href;
499+
500+
// 构建.diff URL
501+
const diffUrl = currentUrl.replace(/\/merge_requests\/(\d+).*$/, '/merge_requests/$1.diff');
502+
503+
// 判断是桌面按钮还是移动端菜单项
504+
const isMobileMenu = event && event.currentTarget && event.currentTarget.classList.contains('gl-new-dropdown-item-content');
505+
506+
// 获取按钮元素和文本元素
507+
let btn, textElement, originalText;
508+
509+
if (isMobileMenu) {
510+
btn = event.currentTarget;
511+
textElement = btn.querySelector('.gl-new-dropdown-item-text-wrapper');
512+
originalText = textElement.textContent;
513+
textElement.textContent = '加载中...';
514+
} else {
515+
btn = document.getElementById('gitlab-copy-diff-btn');
516+
textElement = btn.querySelector('.gl-button-text');
517+
originalText = textElement.textContent;
518+
textElement.textContent = '加载中...';
519+
btn.disabled = true;
520+
}
521+
522+
// 获取diff内容
523+
fetch(diffUrl)
524+
.then(response => {
525+
if (!response.ok) {
526+
throw new Error('获取diff失败');
527+
}
528+
return response.text();
529+
})
530+
.then(diffText => {
531+
// 复制到剪贴板
532+
navigator.clipboard.writeText(diffText)
533+
.then(() => {
534+
// 显示成功状态
535+
textElement.textContent = '复制成功!';
536+
537+
// 创建并显示一个临时的成功提示
538+
showToast('Git diff已复制到剪贴板');
539+
540+
setTimeout(() => {
541+
textElement.textContent = originalText;
542+
if (!isMobileMenu) {
543+
btn.disabled = false;
544+
}
545+
}, 2000);
546+
})
547+
.catch(err => {
548+
console.error('复制失败:', err);
549+
textElement.textContent = '复制失败';
550+
setTimeout(() => {
551+
textElement.textContent = originalText;
552+
if (!isMobileMenu) {
553+
btn.disabled = false;
554+
}
555+
}, 2000);
556+
});
557+
})
558+
.catch(error => {
559+
console.error('获取diff失败:', error);
560+
textElement.textContent = '获取失败';
561+
setTimeout(() => {
562+
textElement.textContent = originalText;
563+
if (!isMobileMenu) {
564+
btn.disabled = false;
565+
}
566+
}, 2000);
567+
});
568+
}
569+
570+
// 显示一个临时的toast提示
571+
function showToast(message) {
572+
// 检查是否已存在toast
573+
let toast = document.querySelector('.gitlab-toast');
574+
if (toast) {
575+
toast.remove();
576+
}
577+
578+
// 创建toast元素
579+
toast = document.createElement('div');
580+
toast.className = 'gitlab-toast';
581+
toast.textContent = message;
582+
toast.style.cssText = `
583+
position: fixed;
584+
bottom: 20px;
585+
left: 50%;
586+
transform: translateX(-50%);
587+
background-color: #1f75cb;
588+
color: white;
589+
padding: 10px 20px;
590+
border-radius: 4px;
591+
z-index: 9999;
592+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
593+
font-size: 14px;
594+
font-weight: 500;
595+
`;
596+
597+
// 添加到页面
598+
document.body.appendChild(toast);
599+
600+
// 2秒后自动消失
601+
setTimeout(() => {
602+
toast.style.opacity = '0';
603+
toast.style.transition = 'opacity 0.5s';
604+
setTimeout(() => toast.remove(), 500);
605+
}, 2000);
606+
}
607+
608+
// 根据页面类型执行不同的操作
609+
function handlePageChange() {
610+
if (window.location.href.includes('/users/') && window.location.href.includes('/activity')) {
611+
insertExportBtn();
612+
} else if (window.location.href.includes('/merge_requests/')) {
613+
insertCopyDiffBtn();
614+
}
615+
}
616+
617+
const observer = new MutationObserver(handlePageChange);
418618
observer.observe(document.body, { childList: true, subtree: true });
419-
insertExportBtn();
619+
handlePageChange();
420620
})();

manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
"content_scripts": [
3030
{
3131
"matches": [
32-
"*://*/users/*/activity"
32+
"*://*/users/*/activity",
33+
"*://*/*/merge_requests/*"
3334
],
3435
"js": ["contentScript.js"],
3536
"run_at": "document_end"

options/options.html

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,18 @@ <h2>插件设置</h2>
1212
<div>
1313
<label>默认 GitLab 实例地址: <input type="text" id="defaultBaseUrl" placeholder="https://gitlab.com" /></label>
1414
</div>
15-
<button id="saveBtn">保存</button>
15+
<div style="margin-top: 10px;">
16+
<label>CSV导出编码:
17+
<select id="csvEncoding">
18+
<option value="utf-8">UTF-8 (通用)</option>
19+
<option value="gbk">GBK (Windows Excel兼容)</option>
20+
</select>
21+
</label>
22+
<div style="font-size: 12px; color: #666; margin-top: 5px;">
23+
提示: Windows用户选择GBK可以避免Excel打开CSV时出现中文乱码
24+
</div>
25+
</div>
26+
<button id="saveBtn" style="margin-top: 15px;">保存</button>
1627
<div id="saveMsg"></div>
1728
<script src="options.js"></script>
1829
</body>

options/options.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,40 @@
11
// options/options.js
22

3+
// 检测是否为Windows系统
4+
function isWindowsOS() {
5+
return navigator.userAgent.indexOf('Windows') !== -1;
6+
}
7+
8+
// 默认编码设置,Windows默认使用GBK,其他系统使用UTF-8
9+
const defaultEncoding = isWindowsOS() ? 'gbk' : 'utf-8';
10+
311
document.addEventListener('DOMContentLoaded', () => {
4-
chrome.storage.sync.get(['defaultToken', 'defaultBaseUrl'], (data) => {
12+
chrome.storage.sync.get(['defaultToken', 'defaultBaseUrl', 'csvEncoding'], (data) => {
513
document.getElementById('defaultToken').value = data.defaultToken || '';
614
document.getElementById('defaultBaseUrl').value = data.defaultBaseUrl || '';
15+
16+
// 设置CSV编码选项
17+
const encodingSelect = document.getElementById('csvEncoding');
18+
encodingSelect.value = data.csvEncoding || defaultEncoding;
19+
20+
// 如果是Windows系统,默认选择GBK
21+
if (!data.csvEncoding && isWindowsOS()) {
22+
encodingSelect.value = 'gbk';
23+
}
724
});
825
});
926

1027
document.getElementById('saveBtn').addEventListener('click', () => {
1128
const defaultToken = document.getElementById('defaultToken').value;
1229
const defaultBaseUrl = document.getElementById('defaultBaseUrl').value;
13-
chrome.storage.sync.set({ defaultToken, defaultBaseUrl }, () => {
30+
const csvEncoding = document.getElementById('csvEncoding').value;
31+
32+
chrome.storage.sync.set({
33+
defaultToken,
34+
defaultBaseUrl,
35+
csvEncoding
36+
}, () => {
1437
document.getElementById('saveMsg').textContent = '保存成功!';
1538
setTimeout(() => document.getElementById('saveMsg').textContent = '', 1500);
1639
});
17-
});
40+
});

utils/export.js

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,57 @@
11
// utils/export.js
22

3-
export function exportToCSV(data, filename = 'events.csv') {
3+
// 检测是否为Windows系统
4+
function isWindowsOS() {
5+
return navigator.userAgent.indexOf('Windows') !== -1;
6+
}
7+
8+
// 默认编码设置,Windows默认使用GBK,其他系统使用UTF-8
9+
const defaultEncoding = isWindowsOS() ? 'gbk' : 'utf-8';
10+
11+
// 获取用户设置的编码
12+
async function getEncodingPreference() {
13+
return new Promise((resolve) => {
14+
chrome.storage.sync.get(['csvEncoding'], (data) => {
15+
resolve(data.csvEncoding || defaultEncoding);
16+
});
17+
});
18+
}
19+
20+
// 添加UTF-8 BOM标记
21+
function addUtf8Bom(content) {
22+
const bom = new Uint8Array([0xEF, 0xBB, 0xBF]);
23+
return new Blob([bom, content], { type: 'text/csv;charset=utf-8' });
24+
}
25+
26+
// 创建Excel兼容的CSV (UTF-8 with BOM)
27+
function createExcelCompatibleCsv(content, encoding) {
28+
if (encoding === 'gbk') {
29+
// 对于Windows/Excel,我们使用UTF-8 with BOM
30+
// 这是因为浏览器环境中直接生成GBK编码内容有限制
31+
// 但UTF-8 with BOM可以被Excel正确识别
32+
return addUtf8Bom(content);
33+
} else {
34+
// 对于其他系统,也使用UTF-8 with BOM以确保兼容性
35+
return addUtf8Bom(content);
36+
}
37+
}
38+
39+
export async function exportToCSV(data, filename = 'events.csv') {
440
const csvRows = [];
541
if (data.length === 0) return;
642
const headers = Object.keys(data[0]);
743
csvRows.push(headers.join(','));
844
for (const row of data) {
945
csvRows.push(headers.map(h => '"' + (row[h] || '').toString().replace(/"/g, '""') + '"').join(','));
1046
}
11-
const blob = new Blob([csvRows.join('\n')], { type: 'text/csv' });
47+
48+
const csvContent = csvRows.join('\n');
49+
const encoding = await getEncodingPreference();
50+
51+
// 创建Excel兼容的CSV
52+
const blob = createExcelCompatibleCsv(csvContent, encoding);
53+
54+
// 创建下载链接
1255
const link = document.createElement('a');
1356
link.href = URL.createObjectURL(blob);
1457
link.download = filename;
@@ -28,7 +71,14 @@ export async function exportToCSVWithProgress(data, setStatus, filename = 'event
2871
setStatus && setStatus(`正在导出... (${Math.min(i + batch, data.length)}/${data.length})`);
2972
await new Promise(r => setTimeout(r, 0));
3073
}
31-
const blob = new Blob([csvRows.join('\n')], { type: 'text/csv' });
74+
75+
const csvContent = csvRows.join('\n');
76+
const encoding = await getEncodingPreference();
77+
78+
// 创建Excel兼容的CSV
79+
const blob = createExcelCompatibleCsv(csvContent, encoding);
80+
81+
// 创建下载链接
3282
const link = document.createElement('a');
3383
link.href = URL.createObjectURL(blob);
3484
link.download = filename;

0 commit comments

Comments
 (0)