// ==UserScript==
// @name CNB Issue 区域选择工具 收藏夹
// @namespace http://tampermonkey.net/
// @version 1.3.1
// @description 选择页面区域并转换为Markdown发送到CNB创建Issue
// @author IIIStudio
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_notification
// @grant GM_setClipboard
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @connect api.cnb.cool
// @connect cnb.cool
// @license MIT
// @downloadURL https://update.greasyfork.icu/scripts/552006/CNB%20Issue%20%E5%8C%BA%E5%9F%9F%E9%80%89%E6%8B%A9%E5%B7%A5%E5%85%B7%20%E6%94%B6%E8%97%8F%E5%A4%B9.user.js
// @updateURL https://update.greasyfork.icu/scripts/552006/CNB%20Issue%20%E5%8C%BA%E5%9F%9F%E9%80%89%E6%8B%A9%E5%B7%A5%E5%85%B7%20%E6%94%B6%E8%97%8F%E5%A4%B9.meta.js
// ==/UserScript==
(function() {
'use strict';
const __CNB_FLAGS = Object.create(null);
function addStyleOnce(key, cssText) {
try {
if (__CNB_FLAGS[key]) return;
if (typeof GM_addStyle === 'function') GM_addStyle(cssText);
__CNB_FLAGS[key] = 1;
} catch (_) {}
}
let __CNB_CLIP_DIALOG = null;
let __CNB_SETTINGS_DIALOG = null, __CNB_SETTINGS_OVERLAY = null;
let __CNB_ISSUE_DIALOG = null, __CNB_ISSUE_OVERLAY = null;
let __CNB_MO = null;
let __CNB_UNLOAD_BOUND = false;
// 配置信息
const CONFIG = {
apiBase: 'https://api.cnb.cool',
repoPath: '',
accessToken: '',
issueEndpoint: '/-/issues'
};
let SAVED_TAGS = [];
// 选择模式快捷键(可在设置中修改),规范格式如:Shift+E
let START_HOTKEY = 'Shift+E';
let HOTKEY_ENABLED = false;
// 添加自定义样式
GM_addStyle(`
.cnb-issue-floating-btn {
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
background: #0366d6;
color: white;
border: none;
border-radius: 50%;
width: 50px;
height: 50px;
cursor: pointer;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.cnb-issue-floating-btn:hover {
background: #0256b9;
transform: scale(1.1);
}
.cnb-issue-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
z-index: 10001;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
min-width: 500px;
max-width: 90vw;
max-height: 80vh;
overflow: auto;
}
.cnb-issue-dialog h3 {
margin: 0 0 15px 0;
color: #333;
}
.cnb-issue-dialog textarea {
width: 100%;
height: 300px;
margin: 10px 0;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
resize: vertical;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
line-height: 1.4;
}
.cnb-issue-dialog input {
width: 100%;
margin: 10px 0;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
.cnb-issue-dialog-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 15px;
}
/* 仅底部操作按钮生效,避免影响设置区的小按钮与“×” */
.cnb-issue-dialog .cnb-issue-dialog-buttons > button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color .15s ease, box-shadow .15s ease, transform .02s ease;
}
.cnb-issue-btn-confirm {
background: #0366d6;
color: white;
}
.cnb-issue-btn-cancel {
background: #6c757d;
color: white;
}
.cnb-issue-btn-confirm:hover {
background: #0256b9;
}
.cnb-issue-btn-cancel:hover {
background: #5a6268;
}
/* 新增:创建完成Issue 按钮样式(绿色) */
.cnb-issue-btn-done {
background: #28a745;
color: white;
}
.cnb-issue-btn-done:hover {
background: #218838;
}
.cnb-issue-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 10000;
}
.cnb-issue-loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #0366d6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 10px;
}
/* 区域选择模式样式 */
.cnb-selection-mode * {
cursor: crosshair !important;
}
.cnb-selection-hover {
outline: 2px solid #0366d6 !important;
background-color: rgba(3, 102, 214, 0.1) !important;
}
.cnb-selection-selected {
outline: 3px solid #28a745 !important;
background-color: rgba(40, 167, 69, 0.15) !important;
}
.cnb-selection-tooltip {
position: fixed;
top: 10px;
left: 50%;
transform: translateX(-50%);
background: #333;
color: white;
padding: 10px 20px;
border-radius: 4px;
z-index: 10002;
font-size: 14px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
.cnb-selection-tooltip button {
margin-left: 10px;
padding: 4px 8px;
background: #28a745;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`);
/* 左侧贴边 Dock 控制栏(自动隐藏,悬停显示) */
GM_addStyle(`
.cnb-dock {
position: fixed;
left: 0;
top: 40%;
transform: translateX(-88%);
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px 8px 8px 12px; /* 左侧保留把手可点区域 */
background: rgba(255,255,255,0.95);
border: 1px solid #d0d7de;
border-left: none;
border-radius: 0 8px 8px 0;
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
z-index: 10002;
transition: transform .2s ease, opacity .2s ease;
opacity: 0;
}
.cnb-dock:hover,
.cnb-dock.cnb-dock--visible {
transform: translateX(0);
opacity: 1;
}
.cnb-dock .cnb-dock-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 72px;
height: 32px;
padding: 0 10px;
font-size: 13px;
color: #24292f;
background: #f6f8fa;
border: 1px solid #d0d7de;
border-radius: 6px;
cursor: pointer;
transition: background-color .15s ease, box-shadow .15s ease, transform .02s ease;
}
.cnb-dock .cnb-dock-btn:hover {
background: #eef2f6;
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
.cnb-dock .cnb-dock-btn:active {
transform: translateY(1px);
box-shadow: 0 1px 3px rgba(0,0,0,0.18);
}
/* 左侧把手提示条 */
.cnb-dock::before {
content: '';
position: absolute;
left: 0;
top: 12px;
width: 10px;
height: calc(100% - 24px);
background: linear-gradient(180deg, #e9ecef, #dde2e7);
border-right: 1px solid #d0d7de;
border-radius: 0 6px 6px 0;
}
`);
// 追加设置按钮样式
GM_addStyle(`
.cnb-issue-settings-btn {
position: fixed;
z-index: 10000;
background: #6c757d;
color: white;
border: none;
border-radius: 50%;
width: 44px;
height: 44px;
cursor: pointer;
box-shadow: 0 2px 10px rgba(0,0,0,0.25);
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.cnb-issue-settings-btn:hover {
background: #5a6268;
transform: scale(1.05);
}
`);
/* 强制隔离并统一控件样式,避免继承站点样式 */
GM_addStyle(`
.cnb-issue-dialog input.cnb-control,
.cnb-issue-dialog textarea.cnb-control {
box-sizing: border-box !important;
width: 100% !important;
margin: 10px 0 !important;
padding: 8px 10px !important;
border: 1px solid #ccc !important;
border-radius: 4px !important;
background: #fff !important;
color: #222 !important;
font: normal 14px/1.4 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Helvetica,Arial,"PingFang SC","Microsoft Yahei",sans-serif !important;
outline: none !important;
appearance: none !important;
-webkit-appearance: none !important;
-moz-appearance: none !important;
}
.cnb-issue-dialog textarea.cnb-control {
min-height: 300px !important;
resize: vertical !important;
font-family: 'Monaco','Menlo','Ubuntu Mono',monospace !important;
font-size: 12px !important;
line-height: 1.4 !important;
}
/* 仅底部操作按钮生效,避免影响设置区的小按钮与“×”
不设置背景和颜色,让各自类(confirm/cancel)决定配色与 hover */
.cnb-issue-dialog .cnb-issue-dialog-buttons > button {
padding: 8px 16px !important;
border: none !important;
border-radius: 4px !important;
cursor: pointer !important;
font-size: 14px !important;
transition: background-color .15s ease, box-shadow .15s ease, transform .02s ease !important;
}
.cnb-issue-btn-confirm { background: #0366d6 !important; color: #fff !important; }
.cnb-issue-btn-confirm:hover { background: #0256b9 !important; box-shadow: 0 2px 6px rgba(0,0,0,0.15) !important; }
.cnb-issue-btn-cancel { background: #6c757d !important; color: #fff !important; }
.cnb-issue-btn-cancel:hover { background: #5a6268 !important; box-shadow: 0 2px 6px rgba(0,0,0,0.15) !important; }
/* 新增:创建完成Issue 按钮(绿色) */
.cnb-issue-btn-done { background: #28a745 !important; color: #fff !important; }
.cnb-issue-btn-done:hover { background: #218838 !important; box-shadow: 0 2px 6px rgba(0,0,0,0.15) !important; }
.cnb-issue-btn-confirm:active, .cnb-issue-btn-cancel:active, .cnb-issue-btn-done:active { transform: translateY(1px) scale(0.98) !important; box-shadow: 0 1px 3px rgba(0,0,0,0.2) !important; }
/* 标签选择按钮 */
#cnb-issue-tags { margin-top: 6px !important; }
.cnb-tag-btn {
margin: 4px !important;
padding: 4px 10px !important;
border: 1px solid #ccc !important;
border-radius: 16px !important;
background: #f8f9fa !important;
color: #222 !important;
font-size: 13px !important;
cursor: pointer !important;
}
.cnb-tag-btn.active {
background: #0366d6 !important;
border-color: #0256b9 !important;
color: #fff !important;
}
/* 设置页:标签胶囊与删除按钮 */
.cnb-tags-list { margin-top: 8px !important; }
.cnb-tag-pill {
display: inline-flex !important;
align-items: center !important;
gap: 6px !important;
margin: 4px !important;
padding: 4px 10px !important;
border: 1px solid #d0d7de !important;
border-radius: 9999px !important;
background: #fff !important;
color: #24292f !important;
font-size: 13px !important;
line-height: 1.2 !important;
white-space: nowrap !important;
vertical-align: middle !important;
box-shadow: 0 1px 0 rgba(27,31,36,0.04) !important;
transition: background-color .15s ease, border-color .15s ease, box-shadow .15s ease !important;
user-select: none !important;
}
.cnb-tag-delbtn {
/* 与通用按钮样式彻底隔离,保持小矩形,仅比“×”略大 */
margin-left: 4px !important;
border: none !important;
background: transparent !important;
cursor: pointer !important;
color: #666 !important;
font-size: 14px !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
height: 20px !important;
padding: 0 6px !important;
line-height: 20px !important;
border-radius: 4px !important;
box-sizing: border-box !important;
white-space: nowrap !important;
min-width: 0 !important; /* 防止被通用按钮样式撑宽 */
}
.cnb-tag-pill:hover {
background: #f6f8fa !important;
border-color: #afb8c1 !important;
box-shadow: 0 1px 0 rgba(27,31,36,0.06) !important;
}
.cnb-tag-delbtn:hover {
color: #cf222e !important;
background: #ffeef0 !important;
}
.cnb-tag-delbtn:active {
background: #ffdce0 !important;
}
/* 设置页:输入与按钮排列 */
.cnb-flex {
display: flex !important;
gap: 8px !important;
align-items: center !important;
flex-wrap: nowrap !important; /* 一行展示,禁止换行 */
}
.cnb-tag-addbtn {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
white-space: nowrap !important;
height: 36px !important; /* 与输入框等高 */
padding: 0 12px !important;
box-sizing: border-box !important;
border-radius: 4px !important;
border: none !important;
background: #28a745 !important;
color: #fff !important;
cursor: pointer !important;
font-size: 14px !important;
flex: 0 0 auto !important; /* 按钮不被压缩,不换行 */
min-width: max-content !important; /* 宽度随文字自适应,避免“添加标/签” */
}
.cnb-tag-addbtn:hover { background: #218838 !important; }
/* 让输入框可伸缩并等高 */
.cnb-flex .cnb-control#cnb-setting-newtag {
height: 36px !important;
flex: 1 1 auto !important;
}
/* 提示文本 */
.cnb-hint {
color: #666 !important;
font-size: 12px !important;
}
/* 开关样式(无文字,仅图形) */
.cnb-switch {
position: relative !important;
display: inline-block !important;
width: 42px !important;
height: 22px !important;
vertical-align: middle !important;
}
.cnb-switch input {
opacity: 0 !important;
width: 0 !important;
height: 0 !important;
position: absolute !important;
}
.cnb-switch-slider {
position: absolute !important;
inset: 0 !important;
background: #c7ccd1 !important;
border-radius: 9999px !important;
transition: background-color .15s ease !important;
box-shadow: inset 0 0 0 1px rgba(0,0,0,0.06) !important;
cursor: pointer !important;
}
.cnb-switch-slider::before {
content: '' !important;
position: absolute !important;
left: 2px !important;
top: 2px !important;
width: 18px !important;
height: 18px !important;
background: #fff !important;
border-radius: 50% !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.2) !important;
transition: transform .15s ease !important;
}
.cnb-switch input:checked + .cnb-switch-slider {
background: #28a745 !important;
}
.cnb-switch input:checked + .cnb-switch-slider::before {
transform: translateX(20px) !important;
}
`);
let isSelecting = false;
let selectedElement = null;
// 多选集合与最近一次选择的元素
let selectedElements = new Set();
let lastSelectedElement = null;
// HTML转Markdown的转换器
const htmlToMarkdown = {
// 转换入口函数
convert: function(html) {
// 创建临时容器
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
// 清理不需要的元素
this.cleanUnwantedElements(tempDiv);
// 递归转换
return this.processNode(tempDiv).trim();
},
// 清理不需要的元素
cleanUnwantedElements: function(element) {
const unwantedSelectors = [
'script', 'style', 'noscript', 'link', 'meta',
'.ads', '.advertisement', '[class*="ad"]',
'.hidden', '[style*="display:none"]', '[style*="display: none"]'
];
unwantedSelectors.forEach(selector => {
const elements = element.querySelectorAll(selector);
elements.forEach(el => el.remove());
});
},
// 处理节点
processNode: function(node) {
if (node.nodeType === Node.TEXT_NODE) {
return this.escapeText(node.textContent || '');
}
if (node.nodeType !== Node.ELEMENT_NODE) {
return '';
}
const tagName = node.tagName.toLowerCase();
const children = Array.from(node.childNodes);
const childrenContent = children.map(child => this.processNode(child)).join('');
switch (tagName) {
case 'h1':
return `# ${childrenContent}\n\n`;
case 'h2':
return `## ${childrenContent}\n\n`;
case 'h3':
return `### ${childrenContent}\n\n`;
case 'h4':
return `#### ${childrenContent}\n\n`;
case 'h5':
return `##### ${childrenContent}\n\n`;
case 'h6':
return `###### ${childrenContent}\n\n`;
case 'p':
return `${childrenContent}\n\n`;
case 'br':
return '\n';
case 'hr':
return '---\n\n';
case 'strong':
case 'b':
return `**${childrenContent}**`;
case 'em':
case 'i':
return `*${childrenContent}*`;
case 'code':
if (node.parentElement.tagName.toLowerCase() === 'pre') {
return childrenContent;
}
return `\`${childrenContent}\``;
case 'pre':
const language = node.querySelector('code')?.className?.replace('language-', '') || '';
const raw = node.textContent || '';
return `\`\`\`${language}\n${raw}\n\`\`\`\n\n`;
case 'a':
const href = node.getAttribute('href') || '';
if (href) {
return `[${childrenContent}](${href})`;
}
return childrenContent;
case 'img':
const src = node.getAttribute('src') || '';
const alt = node.getAttribute('alt') || '';
return ``;
case 'ul':
return `${childrenContent}\n`;
case 'ol':
return `${childrenContent}\n`;
case 'li':
const parentTag = node.parentElement.tagName.toLowerCase();
if (parentTag === 'ol') {
const index = Array.from(node.parentElement.children).indexOf(node) + 1;
return `${index}. ${childrenContent}\n`;
} else {
return `- ${childrenContent}\n`;
}
case 'blockquote':
return `> ${childrenContent.split('\n').join('\n> ')}\n\n`;
case 'table':
const rows = node.querySelectorAll('tr');
let tableContent = '';
// 表头
const headerCells = rows[0]?.querySelectorAll('th, td') || [];
if (headerCells.length > 0) {
tableContent += '| ' + Array.from(headerCells).map(cell => this.processNode(cell).replace(/\n/g, ' ').trim()).join(' | ') + ' |\n';
tableContent += '| ' + Array.from(headerCells).map(() => '---').join(' | ') + ' |\n';
}
// 数据行
for (let i = 1; i < rows.length; i++) {
const cells = rows[i].querySelectorAll('td');
if (cells.length > 0) {
tableContent += '| ' + Array.from(cells).map(cell => this.processNode(cell).replace(/\n/g, ' ').trim()).join(' | ') + ' |\n';
}
}
return tableContent + '\n';
case 'div':
return `${childrenContent}\n`;
default:
return childrenContent;
}
},
// 转义文本
escapeText: function(text) {
return text
.replace(/\*/g, '\\*')
.replace(/_/g, '\\_')
.replace(/`/g, '\\`')
.replace(/\[/g, '\\[')
.replace(/\]/g, '\\]')
.replace(/\(/g, '\\(')
.replace(/\)/g, '\\)')
.replace(/#/g, '\\#')
.replace(/\+/g, '\\+')
.replace(/-/g, '\\-')
.replace(/!/g, '\\!')
.replace(/\|/g, '\\|')
.replace(/\n\s*\n/g, '\n\n')
.replace(/[ \t]+/g, ' ')
.trim();
}
};
// 热键工具:规范化与匹配
function normalizeHotkeyString(s) {
if (!s) return '';
return s.split('+').map(p => p.trim()).filter(Boolean).map(p => {
const up = p.toLowerCase();
if (up === 'ctrl') return 'Control';
if (up === 'control') return 'Control';
if (up === 'meta' || up === 'cmd' || up === 'command') return 'Meta';
if (up === 'alt' || up === 'option') return 'Alt';
if (up === 'shift') return 'Shift';
if (up.length === 1) return up.toUpperCase();
// 常见功能键统一首字母大写
return p[0].toUpperCase() + p.slice(1);
}).join('+');
}
function toDisplayHotkeyString(s) {
if (!s) return '';
return s.replace(/\bControl\b/g, 'Ctrl');
}
function eventToHotkeyString(e) {
const parts = [];
if (e.ctrlKey) parts.push('Control');
if (e.shiftKey) parts.push('Shift');
if (e.altKey) parts.push('Alt');
if (e.metaKey) parts.push('Meta');
let key = e.key;
if (!key) return parts.join('+');
// 忽略纯修饰键
if (['Control','Shift','Alt','Meta'].includes(key)) key = '';
// 统一字母为大写,功能键保持名称
if (key && key.length === 1) key = key.toUpperCase();
if (key === ' ') key = 'Space';
if (key === 'Esc') key = 'Escape';
if (key === 'ArrowLeft' || key === 'ArrowRight' || key === 'ArrowUp' || key === 'ArrowDown') {
// 保持不变
}
return parts.concat(key ? [key] : []).join('+');
}
function matchesHotkey(e, hotkeyStr) {
const want = normalizeHotkeyString(hotkeyStr);
const got = eventToHotkeyString(e);
return want && got === want;
}
function isEditableTarget(el) {
if (!el) return false;
const tag = el.tagName ? el.tagName.toLowerCase() : '';
if (tag === 'input' || tag === 'textarea' || tag === 'select') return true;
if (el.isContentEditable) return true;
return false;
}
function globalHotkeyHandler(e) {
// 避免在输入编辑时触发;对话框/遮罩存在时也不触发
if (!HOTKEY_ENABLED) return;
if (isEditableTarget(e.target)) return;
if (document.querySelector('.cnb-issue-dialog') || document.querySelector('.cnb-issue-overlay')) return;
if (!isSelecting && matchesHotkey(e, START_HOTKEY)) {
e.preventDefault();
startAreaSelection();
}
}
// 创建左侧 Dock(去除拖动,仅点击)
function createFloatingButton() {
const dock = document.createElement('div');
dock.className = 'cnb-dock';
dock.title = '悬停展开,移开隐藏';
const btnSelect = document.createElement('button');
btnSelect.className = 'cnb-dock-btn';
btnSelect.textContent = '选择';
btnSelect.addEventListener('click', (e) => {
e.preventDefault();
startAreaSelection();
});
const btnSettings = document.createElement('button');
btnSettings.className = 'cnb-dock-btn';
btnSettings.textContent = '设置';
btnSettings.addEventListener('click', (e) => {
e.preventDefault();
openSettingsDialog();
});
dock.appendChild(btnSelect);
dock.appendChild(btnSettings);
const btnList = document.createElement('button');
btnList.className = 'cnb-dock-btn';
btnList.textContent = '列表';
btnList.addEventListener('click', (e) => {
e.preventDefault();
openIssueList();
});
dock.appendChild(btnList);
// 剪贴板(根据设置的“剪贴板位置”是否为空来决定是否显示)
let __cnbClipCfg = '';
try { if (typeof GM_getValue === 'function') { const v = GM_getValue('cnbClipboardIssue', ''); __cnbClipCfg = String(v || '').trim(); } } catch (_) {}
if (__cnbClipCfg) {
const btnClipboard = document.createElement('button');
btnClipboard.id = 'cnb-btn-clipboard';
btnClipboard.className = 'cnb-dock-btn';
btnClipboard.textContent = '剪贴板';
btnClipboard.addEventListener('click', (e) => {
e.preventDefault();
if (typeof openClipboardWindow === 'function') {
openClipboardWindow();
}
});
dock.appendChild(btnClipboard);
}
document.body.appendChild(dock);
return dock;
}
// 开始区域选择模式
function startAreaSelection() {
if (isSelecting) return;
isSelecting = true;
document.body.classList.add('cnb-selection-mode');
// 创建提示工具条
const tooltip = document.createElement('div');
tooltip.className = 'cnb-selection-tooltip';
tooltip.innerHTML = `
请点击选择页面区域 (将转换为Markdown格式)
`;
tooltip.id = 'cnb-selection-tooltip';
document.body.appendChild(tooltip);
// 添加事件监听
const confirmBtn = tooltip.querySelector('#cnb-confirm-selection');
const cancelBtn = tooltip.querySelector('#cnb-cancel-selection');
confirmBtn.addEventListener('click', () => {
if (selectedElements && selectedElements.size > 0) {
showIssueDialog(Array.from(selectedElements));
} else {
GM_notification({
text: '请先选择区域(支持 Ctrl+点击多选)',
title: 'CNB Issue工具',
timeout: 3000
});
}
});
cancelBtn.addEventListener('click', stopAreaSelection);
// 添加鼠标移动和点击事件
document.addEventListener('mouseover', handleMouseOver);
document.addEventListener('mouseout', handleMouseOut);
document.addEventListener('click', handleElementClick);
// ESC键取消选择
document.addEventListener('keydown', handleKeyDown);
}
// 停止区域选择模式
function stopAreaSelection() {
isSelecting = false;
document.body.classList.remove('cnb-selection-mode');
// 移除提示工具条
const tooltip = document.getElementById('cnb-selection-tooltip');
if (tooltip) {
document.body.removeChild(tooltip);
}
// 移除样式(包含已选与悬停高亮)
if (selectedElement) {
selectedElement.classList.remove('cnb-selection-selected');
}
const toClear = document.querySelectorAll('.cnb-selection-hover, .cnb-selection-selected');
toClear.forEach(el => {
el.classList.remove('cnb-selection-hover');
el.classList.remove('cnb-selection-selected');
});
selectedElements = new Set();
lastSelectedElement = null;
selectedElement = null;
// 移除事件监听
document.removeEventListener('mouseover', handleMouseOver);
document.removeEventListener('mouseout', handleMouseOut);
document.removeEventListener('click', handleElementClick);
document.removeEventListener('keydown', handleKeyDown);
}
// 处理鼠标悬停
function handleMouseOver(e) {
if (!isSelecting) return;
const element = e.target;
if (!selectedElements.has(element) && !element.closest('.cnb-dock')) {
// 移除之前的高亮
const previousHighlight = document.querySelector('.cnb-selection-hover');
if (previousHighlight) {
previousHighlight.classList.remove('cnb-selection-hover');
}
// 高亮当前元素
element.classList.add('cnb-selection-hover');
}
}
// 处理鼠标移出
function handleMouseOut(e) {
if (!isSelecting) return;
const element = e.target;
if (!selectedElements.has(element) && element.classList.contains('cnb-selection-hover')) {
element.classList.remove('cnb-selection-hover');
}
}
// 处理元素点击
function handleElementClick(e) {
if (!isSelecting) return;
e.preventDefault();
e.stopPropagation();
const element = e.target;
// Ctrl 多选:切换该元素选中状态;否则保持单选
if (e.ctrlKey === true) {
element.classList.remove('cnb-selection-hover');
if (selectedElements.has(element)) {
element.classList.remove('cnb-selection-selected');
selectedElements.delete(element);
} else {
element.classList.add('cnb-selection-selected');
selectedElements.add(element);
lastSelectedElement = element;
}
} else {
selectedElements.forEach(el => el.classList.remove('cnb-selection-selected'));
selectedElements.clear();
selectedElement = element;
selectedElement.classList.remove('cnb-selection-hover');
selectedElement.classList.add('cnb-selection-selected');
selectedElements.add(selectedElement);
lastSelectedElement = selectedElement;
}
// 更新提示信息
const tooltip = document.getElementById('cnb-selection-tooltip');
if (tooltip) {
const tagName = element.tagName.toLowerCase();
const className = element.className ? ` class="${element.className.split(' ')[0]}"` : '';
tooltip.innerHTML = `
已选择: <${tagName}${className}> (将转换为Markdown)
`;
// 重新绑定事件
const confirmBtn = tooltip.querySelector('#cnb-confirm-selection');
const cancelBtn = tooltip.querySelector('#cnb-cancel-selection');
confirmBtn.addEventListener('click', () => {
if (selectedElements && selectedElements.size > 0) {
showIssueDialog(Array.from(selectedElements));
} else if (typeof GM_notification === 'function') {
GM_notification({
text: '请先选择区域(支持 Ctrl+点击多选)',
title: 'CNB Issue工具',
timeout: 3000
});
}
});
cancelBtn.addEventListener('click', stopAreaSelection);
}
}
// 处理按键
function handleKeyDown(e) {
if (e.key === 'Escape') {
stopAreaSelection();
} else if (e.key === 'Enter' || e.key === 'NumpadEnter') {
if (isSelecting && selectedElements && selectedElements.size > 0) {
e.preventDefault();
showIssueDialog(Array.from(selectedElements));
}
}
}
// 显示创建Issue的对话框
function showIssueDialog(selected) {
stopAreaSelection(); // 先退出选择模式
// 创建遮罩层
const overlay = document.createElement('div');
overlay.className = 'cnb-issue-overlay';
// 创建对话框
const dialog = document.createElement('div');
dialog.className = 'cnb-issue-dialog';
// 强化筛选容器为 flex 并固定 5px 间距(避免被站点样式覆盖)
GM_addStyle(`
.cnb-issue-dialog .cnb-issue-filter {
display: flex !important;
flex-wrap: wrap !important;
gap: 5px !important;
}
`);
// 强化筛选标签按钮样式(避免被站点样式覆盖,统一为胶囊风格)
GM_addStyle(`
.cnb-issue-dialog .cnb-issue-filter { display:flex !important; flex-wrap:wrap !important; gap:5px !important; }
.cnb-issue-dialog .cnb-issue-filter .cnb-issue-filter-btn {
display: inline-flex !important;
align-items: center !important;
gap: 6px !important;
padding: 4px 10px !important;
border: 1px solid #d0d7de !important;
border-radius: 9999px !important;
background: #fff !important;
color: #24292f !important;
font-size: 13px !important;
line-height: 1.2 !important;
white-space: nowrap !important;
vertical-align: middle !important;
box-shadow: 0 1px 0 rgba(27,31,36,0.04) !important;
transition: background-color .15s ease, border-color .15s ease, box-shadow .15s ease, transform .02s ease !important;
cursor: pointer !important;
user-select: none !important;
}
.cnb-issue-dialog .cnb-issue-filter .cnb-issue-filter-btn:hover {
background: #f6f8fa !important;
border-color: #afb8c1 !important;
box-shadow: 0 1px 0 rgba(27,31,36,0.06) !important;
}
.cnb-issue-dialog .cnb-issue-filter .cnb-issue-filter-btn.active {
background: #0366d6 !important;
border-color: #0256b9 !important;
color: #fff !important;
box-shadow: 0 1px 0 rgba(27,31,36,0.05) !important;
}
.cnb-issue-dialog .cnb-issue-filter .cnb-issue-filter-btn.pressed {
transform: translateY(1px) scale(0.98) !important;
box-shadow: 0 1px 0 rgba(27,31,36,0.08) !important;
}
`);
// 获取选择的内容并转换为Markdown(支持多选)
const elements = Array.isArray(selected) ? selected : (selected ? [selected] : []);
const parts = elements.map(el => (getSelectedContentAsMarkdown(el) || '').trim()).filter(Boolean);
const joined = parts.join(`
---
`);
const selectedContent = (parts.length > 1 ? `
` : '') + joined;
const pageTitle = document.title;
const pageUrl = window.location.href;
dialog.innerHTML = `
创建 CNB Issue (Markdown格式)
`;
// 添加事件监听
// 渲染标签为可选按钮
const tagsContainer = dialog.querySelector('#cnb-issue-tags');
let selectedTags = [];
if (tagsContainer) {
tagsContainer.innerHTML = '';
const tags = Array.isArray(SAVED_TAGS) ? SAVED_TAGS : [];
if (tags.length === 0) {
const hint = document.createElement('div');
hint.className = 'cnb-hint';
hint.textContent = '在设置中添加标签后可在此选择';
tagsContainer.appendChild(hint);
} else {
tags.forEach(tag => {
const btnTag = document.createElement('button');
btnTag.type = 'button';
btnTag.className = 'cnb-tag-btn';
btnTag.textContent = tag;
btnTag.addEventListener('click', () => {
const idx = selectedTags.indexOf(tag);
if (idx >= 0) {
selectedTags.splice(idx, 1);
btnTag.classList.remove('active');
} else {
selectedTags.push(tag);
btnTag.classList.add('active');
}
});
tagsContainer.appendChild(btnTag);
});
}
}
const cancelBtn = dialog.querySelector('.cnb-issue-btn-cancel');
const confirmBtn = dialog.querySelector('.cnb-issue-btn-confirm');
const doneBtn = dialog.querySelector('.cnb-issue-btn-done');
const closeDialog = () => {
if (document.body.contains(overlay)) document.body.removeChild(overlay);
if (document.body.contains(dialog)) document.body.removeChild(dialog);
};
overlay.addEventListener('click', closeDialog);
cancelBtn.addEventListener('click', closeDialog);
confirmBtn.addEventListener('click', () => {
const title = dialog.querySelector('#cnb-issue-title').value;
const content = dialog.querySelector('#cnb-issue-content').value;
const labels = Array.isArray(selectedTags) ? selectedTags.slice() : [];
// 禁用按钮并显示加载状态
confirmBtn.disabled = true;
confirmBtn.innerHTML = '创建中...';
createIssue(title, content, labels, (success) => {
if (success) {
closeDialog();
} else {
// 重新启用按钮
confirmBtn.disabled = false;
confirmBtn.innerHTML = '创建Issue';
}
});
});
if (doneBtn) {
doneBtn.addEventListener('click', () => {
const title = dialog.querySelector('#cnb-issue-title').value;
const content = dialog.querySelector('#cnb-issue-content').value;
const labels = Array.isArray(selectedTags) ? selectedTags.slice() : [];
doneBtn.disabled = true;
confirmBtn.disabled = true;
doneBtn.innerHTML = '创建并完成中...';
createIssue(title, content, labels, (success, issueId) => {
if (success && issueId != null) {
closeIssue(issueId, 'completed', (ok) => {
if (!ok) {
doneBtn.disabled = false;
confirmBtn.disabled = false;
doneBtn.innerHTML = '创建完成Issue';
return;
}
if (typeof GM_notification === 'function') {
GM_notification({
text: 'Issue已标记为已完成(closed: completed)',
title: 'CNB Issue工具',
timeout: 3000
});
}
if (document.body.contains(overlay)) document.body.removeChild(overlay);
if (document.body.contains(dialog)) document.body.removeChild(dialog);
});
} else {
doneBtn.disabled = false;
confirmBtn.disabled = false;
doneBtn.innerHTML = '创建完成Issue';
}
});
});
}
document.body.appendChild(overlay);
document.body.appendChild(dialog);
// 自动聚焦到标题输入框
dialog.querySelector('#cnb-issue-title').focus();
dialog.querySelector('#cnb-issue-title').select();
}
// 获取选择区域的内容并转换为Markdown
function getSelectedContentAsMarkdown(element) {
if (!element) return '';
try {
// 获取元素的HTML内容
const htmlContent = element.innerHTML;
// 转换为Markdown
const markdownContent = htmlToMarkdown.convert(htmlContent);
// 清理和格式化
return cleanMarkdownContent(markdownContent);
} catch (error) {
console.error('转换Markdown失败:', error);
// 如果转换失败,回退到纯文本
return element.textContent || element.innerText || '';
}
}
// 清理Markdown内容
function cleanMarkdownContent(markdown) {
return markdown
.replace(/\n{3,}/g, '\n\n') // 多个空行合并为两个
.replace(/^\s+|\s+$/g, ''); // 去除首尾空白
}
// 轻量 Markdown 转 HTML(基础语法)
function markdownToHtml(md) {
if (!md) return '';
let placeholders = [];
// 保护代码块 ```lang\n...\n```
md = md.replace(/```(\w+)?\n([\s\S]*?)```/g, function(_, lang, code) {
const idx = placeholders.length;
const esc = (s)=>String(s).replace(/&/g,'&').replace(//g,'>');
placeholders.push(`${esc(code)}
`);
return `\u0000BLOCK${idx}\u0000`;
});
// 保护行内代码 `code`
md = md.replace(/`([^`\n]+)`/g, function(_, code){
const idx = placeholders.length;
const esc = (s)=>String(s).replace(/&/g,'&').replace(//g,'>');
placeholders.push(`${esc(code)}
`);
return `\u0000INLINE${idx}\u0000`;
});
// 先整体转义,避免 HTML 注入
md = md.replace(/&/g,'&').replace(//g,'>');
// 图片与链接
md = md.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '
');
md = md.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1');
// 粗体/斜体
md = md.replace(/\*\*([^*]+)\*\*/g, '$1');
md = md.replace(/\*([^*]+)\*/g, '$1');
// 标题
md = md.replace(/^(#{6})\s+(.+)$/gm, '$2
')
.replace(/^(#{5})\s+(.+)$/gm, '$2
')
.replace(/^(#{4})\s+(.+)$/gm, '$2
')
.replace(/^(#{3})\s+(.+)$/gm, '$2
')
.replace(/^(#{2})\s+(.+)$/gm, '$2
')
.replace(/^(#{1})\s+(.+)$/gm, '$2
');
// 水平线
md = md.replace(/^\s*[-*_]{3,}\s*$/gm, '
');
// 引用
md = md.replace(/^(?:>\s?(.*))$/gm, '$1
');
// 列表(连续项聚合)
md = md.replace(/(?:^(?:\s*-\s+.+)\n?)+/gm, function(block){
const items = block.trim().split(/\n/).map(l => l.replace(/^\s*-\s+/, '').trim());
return '' + items.map(i=>`- ${i}
`).join('') + '
';
});
md = md.replace(/(?:^(?:\s*\d+\.\s+.+)\n?)+/gm, function(block){
const items = block.trim().split(/\n/).map(l => l.replace(/^\s*\d+\.\s+/, '').trim());
return '' + items.map(i=>`- ${i}
`).join('') + '
';
});
// 段落:双换行分段,避免已是块级元素再次包裹
const blocks = md.split(/\n{2,}/).map(seg=>{
if (/^\s*<(h\d|ul|ol|li|pre|blockquote|hr)/i.test(seg)) return seg;
return '' + seg.replace(/\n/g, '
') + '
';
});
let html = blocks.join('\n');
// 还原占位
html = html.replace(/\u0000(INLINE|BLOCK)(\d+)\u0000/g, (_, type, i) => placeholders[Number(i)] || '');
return html;
}
// HTML转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 设置弹窗
function openSettingsDialog() {
// 单例:若已存在,先移除旧实例
try {
if (__CNB_SETTINGS_OVERLAY && __CNB_SETTINGS_OVERLAY.parentNode) __CNB_SETTINGS_OVERLAY.remove();
if (__CNB_SETTINGS_DIALOG && __CNB_SETTINGS_DIALOG.parentNode) __CNB_SETTINGS_DIALOG.remove();
} catch (_) {}
const overlay = document.createElement('div');
overlay.className = 'cnb-issue-overlay';
const dialog = document.createElement('div');
dialog.className = 'cnb-issue-dialog';
__CNB_SETTINGS_OVERLAY = overlay;
__CNB_SETTINGS_DIALOG = dialog;
const currentRepo = CONFIG.repoPath || '';
const currentToken = CONFIG.accessToken || '';
const currentHotkey = START_HOTKEY || '';
const currentHotkeyEnabled = !!HOTKEY_ENABLED;
let currentClipIssue = '';
try {
if (typeof GM_getValue === 'function') {
const v = GM_getValue('cnbClipboardIssue', '');
currentClipIssue = (v == null) ? '' : String(v);
}
} catch (_) {}
dialog.innerHTML = `
CNB 设置
`;
// 渲染与管理标签
const tagsList = dialog.querySelector('#cnb-setting-tags-list');
const newTagInput = dialog.querySelector('#cnb-setting-newtag');
const addTagBtn = dialog.querySelector('#cnb-setting-addtag');
const hotkeyInput = dialog.querySelector('#cnb-setting-hotkey');
const hotkeyEnabledInput = dialog.querySelector('#cnb-setting-hotkey-enabled');
if (hotkeyEnabledInput) {
hotkeyEnabledInput.addEventListener('change', () => {
HOTKEY_ENABLED = !!hotkeyEnabledInput.checked;
if (typeof GM_setValue === 'function') GM_setValue('cnbHotkeyEnabled', HOTKEY_ENABLED);
});
}
// 录制快捷键:在输入框中按组合键即生成规范字符串
if (hotkeyInput) {
hotkeyInput.addEventListener('keydown', (e) => {
e.preventDefault();
const str = eventToHotkeyString(e);
hotkeyInput.value = toDisplayHotkeyString(normalizeHotkeyString(str));
});
}
// 回车键添加标签
newTagInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
addTagBtn.click();
}
});
function renderTagsList() {
tagsList.innerHTML = '';
const tags = Array.isArray(SAVED_TAGS) ? SAVED_TAGS : [];
if (tags.length === 0) {
const empty = document.createElement('div');
empty.className = 'cnb-hint';
empty.textContent = '暂无标签';
tagsList.appendChild(empty);
return;
}
tags.forEach((tag, idx) => {
const item = document.createElement('span');
item.textContent = tag;
item.className = 'cnb-tag-pill';
const del = document.createElement('button');
del.type = 'button';
del.textContent = '×';
del.title = '删除';
del.className = 'cnb-tag-delbtn';
del.addEventListener('click', () => {
SAVED_TAGS.splice(idx, 1);
if (typeof GM_setValue === 'function') GM_setValue('cnbTags', SAVED_TAGS);
renderTagsList();
});
item.appendChild(del);
tagsList.appendChild(item);
});
}
renderTagsList();
addTagBtn.addEventListener('click', () => {
const t = (newTagInput.value || '').trim();
if (!t) return;
if (!Array.isArray(SAVED_TAGS)) SAVED_TAGS = [];
if (!SAVED_TAGS.includes(t)) {
SAVED_TAGS.push(t);
if (typeof GM_setValue === 'function') GM_setValue('cnbTags', SAVED_TAGS);
renderTagsList();
newTagInput.value = '';
if (typeof GM_notification === 'function') {
GM_notification({ text: '标签已添加', title: 'CNB Issue工具', timeout: 1500 });
}
}
});
const close = () => {
if (document.body.contains(overlay)) document.body.removeChild(overlay);
if (document.body.contains(dialog)) document.body.removeChild(dialog);
__CNB_SETTINGS_OVERLAY = null;
__CNB_SETTINGS_DIALOG = null;
};
dialog.querySelector('.cnb-issue-btn-cancel').addEventListener('click', close);
overlay.addEventListener('click', close);
dialog.querySelector('.cnb-issue-btn-confirm').addEventListener('click', () => {
const repo = dialog.querySelector('#cnb-setting-repo').value.trim();
const token = dialog.querySelector('#cnb-setting-token').value.trim();
const hotkey = (dialog.querySelector('#cnb-setting-hotkey')?.value || '').trim();
const hotkeyEnabled = !!(dialog.querySelector('#cnb-setting-hotkey-enabled')?.checked);
if (repo) {
CONFIG.repoPath = repo;
if (typeof GM_setValue === 'function') GM_setValue('repoPath', repo);
}
if (token) {
CONFIG.accessToken = token;
if (typeof GM_setValue === 'function') GM_setValue('accessToken', token);
}
if (hotkey) {
START_HOTKEY = normalizeHotkeyString(hotkey);
if (typeof GM_setValue === 'function') GM_setValue('cnbHotkey', START_HOTKEY);
}
HOTKEY_ENABLED = hotkeyEnabled;
if (typeof GM_setValue === 'function') GM_setValue('cnbHotkeyEnabled', HOTKEY_ENABLED);
// 保存剪贴板位置(允许留空,留空则隐藏剪贴板按钮)
try {
const clipIssue = (dialog.querySelector('#cnb-setting-clip-issue')?.value || '').trim();
if (typeof GM_setValue === 'function') GM_setValue('cnbClipboardIssue', clipIssue);
// 即时生效:根据是否有值来动态增删“剪贴板”按钮
const dock = document.querySelector('.cnb-dock');
if (dock) {
let btn = dock.querySelector('#cnb-btn-clipboard');
if (clipIssue) {
if (!btn) {
const btnClipboard = document.createElement('button');
btnClipboard.id = 'cnb-btn-clipboard';
btnClipboard.className = 'cnb-dock-btn';
btnClipboard.textContent = '剪贴板';
btnClipboard.addEventListener('click', (e) => {
e.preventDefault();
if (typeof openClipboardWindow === 'function') {
openClipboardWindow();
}
});
dock.appendChild(btnClipboard);
}
} else {
if (btn) btn.remove();
}
}
} catch (_) {}
if (typeof GM_notification === 'function') {
GM_notification({
text: '设置已保存',
title: 'CNB Issue工具',
timeout: 2000
});
}
close();
});
document.body.appendChild(overlay);
document.body.appendChild(dialog);
}
// Issue 列表弹窗
function openIssueList() {
// 单例:若已存在,先移除旧实例
try {
if (__CNB_ISSUE_OVERLAY && __CNB_ISSUE_OVERLAY.parentNode) __CNB_ISSUE_OVERLAY.remove();
if (__CNB_ISSUE_DIALOG && __CNB_ISSUE_DIALOG.parentNode) __CNB_ISSUE_DIALOG.remove();
} catch (_) {}
if (!CONFIG.repoPath || !CONFIG.accessToken) {
if (typeof GM_notification === 'function') {
GM_notification({ text: '请先在设置中配置仓库路径与访问令牌', title: 'CNB Issue工具', timeout: 3000 });
}
if (typeof openSettingsDialog === 'function') openSettingsDialog();
return;
}
const overlay = document.createElement('div');
overlay.className = 'cnb-issue-overlay';
__CNB_ISSUE_OVERLAY = overlay;
const dialog = document.createElement('div');
dialog.className = 'cnb-issue-dialog';
__CNB_ISSUE_DIALOG = dialog;
dialog.innerHTML = `
Issue 列表
显示 state=closed 的最近 100 条
`;
// 固定对话框尺寸,防止点击筛选按钮时窗口抖动
dialog.style.width = '840px';
dialog.style.maxWidth = '840px';
// 补充:筛选按钮按压态样式
GM_addStyle(`
.cnb-issue-filter-btn.pressed {
transform: translateY(1px) scale(0.98);
box-shadow: 0 1px 0 rgba(27,31,36,0.08);
}
.cnb-issue-filter-btn {
transition: background-color .15s ease, border-color .15s ease, box-shadow .15s ease, transform .02s ease;
}
`);
const listEl = dialog.querySelector('#cnb-issue-list');
const closeBtn = dialog.querySelector('.cnb-dialog-close');
// 行内标签(Issue 列表中的 labels)胶囊样式
GM_addStyle(`
.cnb-issue-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 2px 8px;
border: 1px solid #d0d7de;
border-radius: 9999px;
background: #fff;
color: #24292f;
font-size: 12px;
line-height: 1.2;
white-space: nowrap;
vertical-align: middle;
box-shadow: 0 1px 0 rgba(27,31,36,0.04);
transition: background-color .15s ease, border-color .15s ease, box-shadow .15s ease, transform .02s ease;
user-select: none;
}
.cnb-issue-chip:hover {
background: #f6f8fa;
border-color: #afb8c1;
box-shadow: 0 1px 0 rgba(27,31,36,0.06);
}
`);
const close = () => {
if (document.body.contains(overlay)) document.body.removeChild(overlay);
if (document.body.contains(dialog)) document.body.removeChild(dialog);
document.removeEventListener('keydown', onEsc, true);
__CNB_ISSUE_OVERLAY = null;
__CNB_ISSUE_DIALOG = null;
};
overlay.addEventListener('click', close);
if (closeBtn) closeBtn.addEventListener('click', close);
// ESC 关闭
const onEsc = (e) => {
if (e.key === 'Escape') {
e.preventDefault();
close();
}
};
document.addEventListener('keydown', onEsc, true);
// 初始加载中
listEl.innerHTML = `加载中...
`;
const url = `${CONFIG.apiBase}/${CONFIG.repoPath}${CONFIG.issueEndpoint}?page=1&page_size=100&state=closed`;
GM_xmlhttpRequest({
method: 'GET',
url: url.replace(/&/g, '&'),
headers: {
'accept': 'application/json',
'Authorization': `${CONFIG.accessToken}`
},
responseType: 'json',
onload: function(res) {
try {
const data = typeof res.response === 'object' && res.response !== null
? res.response
: JSON.parse(res.responseText || '[]');
const items = Array.isArray(data) ? data : (Array.isArray(data.items) ? data.items : []);
if (!items.length) {
listEl.innerHTML = `暂无数据
`;
return;
}
// 渲染 + 筛选
const allItems = Array.isArray(items) ? items : [];
const filterEl = dialog.querySelector('#cnb-issue-filter');
// 行内样式强制为 flex 并设置 5px 间距,避免被站点覆盖
if (filterEl) {
const s = filterEl.style;
s.setProperty('display', 'flex', 'important');
s.setProperty('flex-wrap', 'wrap', 'important');
s.setProperty('gap', '5px', 'important');
}
function render(filterLabel) {
const frag = document.createDocumentFragment();
const filtered = !filterLabel ? allItems : allItems.filter(it => {
const names = Array.isArray(it.labels) ? it.labels.map(l => l.name) : [];
return names.includes(filterLabel);
});
filtered.forEach(it => {
const number = it.number ?? it.id ?? it.iid ?? '';
const title = it.title ?? '';
const createdAt = it.created_at ?? '';
const labelNames = Array.isArray(it.labels) ? it.labels.map(l => l.name) : [];
const row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border-bottom:1px solid #eef2f6;';
const left = document.createElement('div');
left.style.cssText = 'min-width:0;flex:1;font-size:14px;color:#24292f;display:flex;gap:5px !important;align-items:center;';
const prefix = document.createElement('span');
prefix.textContent = `#${number}`;
const a = document.createElement('a');
a.href = `https://cnb.cool/${CONFIG.repoPath}/-/issues/${number}`;
a.target = '_blank';
a.rel = 'noopener noreferrer';
const fullTitle = String(title || '');
const truncated = fullTitle.length > 40 ? fullTitle.slice(0, 40) + '…' : fullTitle;
a.textContent = truncated;
a.title = fullTitle;
a.style.cssText = 'color:#0969da;text-decoration:none;word-break:break-all;';
a.addEventListener('mouseover', () => a.style.textDecoration = 'underline');
a.addEventListener('mouseout', () => a.style.textDecoration = 'none');
left.appendChild(prefix);
left.appendChild(a);
// 复制按钮:关闭 Issue(完成) 并复制 title + body(清理为Markdown) 到剪贴板
const btnCopy = document.createElement('button');
btnCopy.type = 'button';
btnCopy.textContent = '📋';
btnCopy.title = '复制到剪贴板';
btnCopy.style.cssText = 'margin-left:6px;display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:#57606a;font-size:12px;cursor:pointer;line-height:1;';
btnCopy.addEventListener('mouseover', () => btnCopy.style.opacity = '0.75');
btnCopy.addEventListener('mouseout', () => btnCopy.style.opacity = '1');
btnCopy.addEventListener('click', () => {
if (btnCopy.disabled) return;
btnCopy.disabled = true;
const oldText = btnCopy.textContent;
btnCopy.textContent = '…';
const urlPatch = `${CONFIG.apiBase}/${CONFIG.repoPath}${CONFIG.issueEndpoint}/${number}`;
GM_xmlhttpRequest({
method: 'PATCH',
url: urlPatch,
headers: {
'Content-Type': 'application/json',
'Authorization': `${CONFIG.accessToken}`,
'Accept': 'application/json'
},
data: JSON.stringify({ state: 'closed', state_reason: 'completed' }),
responseType: 'json',
onload: function(res) {
try {
if (res.status >= 200 && res.status < 300) {
let obj = null;
try {
obj = (typeof res.response === 'object' && res.response !== null)
? res.response
: JSON.parse(res.responseText || '{}');
} catch(_) {}
const t = (obj && obj.title) ? obj.title : title;
const b = (obj && typeof obj.body === 'string') ? obj.body : '';
const md = cleanMarkdownContent(String(b || ''));
if (typeof GM_setClipboard === 'function') {
GM_setClipboard(`${t}
${md}`, 'text');
}
if (typeof GM_notification === 'function') {
GM_notification({ text: '已关闭并复制到剪贴板', title: 'CNB Issue工具', timeout: 3000 });
}
} else {
if (typeof GM_notification === 'function') {
GM_notification({ text: '操作失败: HTTP ' + res.status, title: 'CNB Issue工具', timeout: 5000 });
}
}
} finally {
btnCopy.disabled = false;
btnCopy.textContent = oldText;
}
},
onerror: function() {
if (typeof GM_notification === 'function') {
GM_notification({ text: '网络请求失败', title: 'CNB Issue工具', timeout: 5000 });
}
btnCopy.disabled = false;
btnCopy.textContent = oldText;
}
});
});
left.appendChild(btnCopy);
const right = document.createElement('div');
right.style.cssText = 'flex:0 0 auto;color:#57606a;font-size:12px;text-align:right;display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end;';
// 标签胶囊容器
const labelsWrap = document.createElement('div');
labelsWrap.style.cssText = 'display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end;';
const labelObjs = Array.isArray(it.labels) ? it.labels : [];
labelObjs.forEach(l => {
const chip = document.createElement('span');
chip.className = 'cnb-issue-chip';
chip.textContent = l?.name ?? '';
// 若有颜色,应用为背景,并适当设置边框与前景色
const color = (l && typeof l.color === 'string' && l.color) ? l.color : '';
if (color) {
chip.style.background = color;
// 根据背景亮度调整文字与边框(简单阈值)
try {
const hex = color.replace('#','');
const r = parseInt(hex.substring(0,2),16);
const g = parseInt(hex.substring(2,4),16);
const b = parseInt(hex.substring(4,6),16);
const lum = 0.2126*r + 0.7152*g + 0.0722*b;
chip.style.color = lum < 140 ? '#fff' : '#24292f';
chip.style.borderColor = lum < 140 ? 'rgba(255,255,255,0.35)' : '#d0d7de';
} catch(_) {}
}
labelsWrap.appendChild(chip);
});
const dateSpan = document.createElement('span');
dateSpan.textContent = createdAt;
dateSpan.style.cssText = 'color:#57606a;';
right.appendChild(labelsWrap);
right.appendChild(dateSpan);
row.appendChild(left);
row.appendChild(right);
frag.appendChild(row);
});
listEl.innerHTML = '';
listEl.appendChild(frag);
}
// 渲染筛选按钮
if (filterEl) {
filterEl.innerHTML = '';
const allBtn = document.createElement('button');
allBtn.className = 'cnb-issue-filter-btn active';
allBtn.textContent = '全部';
applyFilterButtonStyles(allBtn);
applyFilterButtonActive(allBtn);
addPressEffect(allBtn);
allBtn.addEventListener('click', () => {
setActive(allBtn);
render(null);
});
filterEl.appendChild(allBtn);
const tagList = Array.isArray(SAVED_TAGS) ? SAVED_TAGS : [];
tagList.forEach(tag => {
const b = document.createElement('button');
b.className = 'cnb-issue-filter-btn';
b.textContent = tag;
applyFilterButtonStyles(b);
addPressEffect(b);
b.addEventListener('click', () => {
setActive(b);
render(tag);
});
filterEl.appendChild(b);
});
function setActive(btn) {
const buttons = filterEl.querySelectorAll('button');
buttons.forEach(x => {
x.classList.remove('active');
applyFilterButtonDefault(x);
});
btn.classList.add('active');
applyFilterButtonActive(btn);
}
// 行内样式(带 !important)确保胶囊风格不被站点覆盖
function applyFilterButtonStyles(btn) {
const s = btn.style;
s.setProperty('display', 'inline-flex', 'important');
s.setProperty('align-items', 'center', 'important');
s.setProperty('gap', '6px', 'important');
s.setProperty('padding', '4px 10px', 'important');
s.setProperty('border', '1px solid #d0d7de', 'important');
s.setProperty('border-radius', '9999px', 'important');
s.setProperty('background', '#fff', 'important');
s.setProperty('color', '#24292f', 'important');
s.setProperty('font-size', '13px', 'important');
s.setProperty('line-height', '1.2', 'important');
s.setProperty('white-space', 'nowrap', 'important');
s.setProperty('vertical-align', 'middle', 'important');
s.setProperty('box-shadow', '0 1px 0 rgba(27,31,36,0.04)', 'important');
s.setProperty('transition', 'background-color .15s ease, border-color .15s ease, box-shadow .15s ease, transform .02s ease', 'important');
s.setProperty('cursor', 'pointer', 'important');
s.setProperty('user-select', 'none', 'important');
// 关键:移除按钮自身外边距,确保由容器 gap 控制间距
s.setProperty('margin', '0', 'important');
}
function applyFilterButtonDefault(btn) {
const s = btn.style;
s.setProperty('background', '#fff', 'important');
s.setProperty('border-color', '#d0d7de', 'important');
s.setProperty('color', '#24292f', 'important');
s.setProperty('box-shadow', '0 1px 0 rgba(27,31,36,0.04)', 'important');
}
function applyFilterButtonActive(btn) {
const s = btn.style;
s.setProperty('background', '#0366d6', 'important');
s.setProperty('border-color', '#0256b9', 'important');
s.setProperty('color', '#fff', 'important');
s.setProperty('box-shadow', '0 1px 0 rgba(27,31,36,0.05)', 'important');
}
// 为筛选按钮添加按压反馈
function addPressEffect(btn) {
btn.addEventListener('mousedown', () => btn.classList.add('pressed'));
btn.addEventListener('mouseup', () => btn.classList.remove('pressed'));
btn.addEventListener('mouseleave', () => btn.classList.remove('pressed'));
btn.addEventListener('touchstart', () => btn.classList.add('pressed'), { passive: true });
btn.addEventListener('touchend', () => btn.classList.remove('pressed'));
btn.addEventListener('touchcancel', () => btn.classList.remove('pressed'));
btn.addEventListener('blur', () => btn.classList.remove('pressed'));
}
}
// 初次渲染全部
render(null);
} catch (e) {
listEl.innerHTML = `解析失败
`;
}
},
onerror: function() {
listEl.innerHTML = `网络请求失败
`;
}
});
document.body.appendChild(overlay);
document.body.appendChild(dialog);
}
// 剪贴板弹窗(独立样式),展示 Issue #25
function openClipboardWindow() {
// 单例:若已存在旧窗口,先移除
try { if (__CNB_CLIP_DIALOG && __CNB_CLIP_DIALOG.parentNode) __CNB_CLIP_DIALOG.remove(); } catch (_) {}
try {
// 注入独立样式(不复用 .cnb-issue-dialog),无遮罩,默认居中,可拖动
addStyleOnce('clipwin-base', `
.cnb-clipwin {
position: fixed;
left: 50%; top: 50%;
transform: translate(-50%, -50%);
width: min(320px, 92vw);
max-height: 80vh;
display: flex; flex-direction: column;
background: #ffffff;
border: 1px solid #d0d7de;
border-radius: 12px;
box-shadow: 0 12px 32px rgba(0,0,0,0.18);
z-index: 10010;
overflow: hidden;
font: 14px/1.5 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Helvetica,Arial,"PingFang SC","Microsoft Yahei",sans-serif;
color: #24292f;
}
.cnb-clipwin-header {
position: relative;
height: 30px;
border-bottom: 1px solid #eaeef2;
background: linear-gradient(180deg, #fbfdff, #f6f8fa);
cursor: move; /* 拖动条 */
}
.cnb-clipwin-close {
position: absolute;
right: 40px; top: 3px;
border: none; background: transparent;
color: #57606a; font-size: 18px; line-height: 1;
cursor: pointer;
}
.cnb-clipwin-pin {
position: absolute;
right: 6px; top: 6px;
border: none; background: transparent;
color: #57606a; line-height: 1;
cursor: pointer;
padding: 2px;
}
/* 左上角标题 */
.cnb-clipwin-title {
position: absolute;
left: 8px; top: 8px;
border: none; background: transparent;
color: #24292f; line-height: 1; font-size: 12.5px; font-weight: 600;
pointer-events: none;
}
.cnb-clipwin-close:hover, .cnb-clipwin-pin:hover { color: #24292f; }
.menuBar-Btn_Icon-pin.isActive { fill: #0366d6; }
.cnb-clipwin-content {
padding: 8px 12px;
overflow: auto;
flex: 1 1 auto;
}
.cnb-clipwin-body {
margin: 0;
padding: 10px;
background: #f6f8fa;
border: 1px solid #d0d7de;
border-radius: 8px;
white-space: pre-wrap;
word-break: break-word;
font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
font-size: 12px;
max-height: 60vh;
overflow: auto;
}
.cnb-clipwin-actions {
border-top: 1px solid #eaeef2;
padding: 10px 16px;
display: flex; gap: 8px; justify-content: flex-end;
background: #fff;
}
.cnb-clipwin-btn {
display: inline-flex; align-items: center; justify-content: center;
height: 32px; padding: 0 12px; border-radius: 8px;
border: 1px solid #d0d7de; background: #f6f8fa; color: #24292f;
cursor: pointer; transition: background-color .15s ease, box-shadow .15s ease, transform .02s ease;
}
.cnb-clipwin-btn:hover { background: #eef2f6; box-shadow: 0 2px 6px rgba(0,0,0,0.12); }
.cnb-clipwin-btn:active { transform: translateY(1px); box-shadow: 0 1px 3px rgba(0,0,0,0.18); }
`);
} catch (_) {}
// 覆盖剪贴板窗口内容样式,适配 HTML 渲染
try {
addStyleOnce('clipwin-body', `
.cnb-clipwin-body {
margin: 0;
padding: 12px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
word-break: break-word;
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Helvetica,Arial,"PingFang SC","Microsoft Yahei",sans-serif;
font-size: 13px;
line-height: 1.55;
color: #24292f;
max-height: 65vh;
overflow: auto;
}
.cnb-clipwin-body h1 { font-size: 1.75em; margin: .4em 0; }
.cnb-clipwin-body h2 { font-size: 1.5em; margin: .6em 0 .4em; }
.cnb-clipwin-body h3 { font-size: 1.25em; margin: .6em 0 .4em; }
.cnb-clipwin-body p { margin: .4em 0; }
.cnb-clipwin-body ul, .cnb-clipwin-body ol { padding-left: 1.5em; margin: .4em 0; }
.cnb-clipwin-body blockquote {
margin: .6em 0; padding: .4em .8em; color:#57606a; background:#f6f8fa; border-left: 4px solid #d0d7de; border-radius: 4px;
}
.cnb-clipwin-body hr { border: none; border-top: 1px solid #e5e7eb; margin: .8em 0; }
.cnb-clipwin-body code {
background: #f6f8fa; border: 1px solid #e5e7eb; border-radius: 4px; padding: .1em .35em; font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; font-size: .92em;
}
.cnb-clipwin-body pre {
background: #0b1021; color: #e6edf3; border-radius: 8px; padding: 8px 0px 8px 8px; overflow-x: hidden; overflow-y: auto;
}
.cnb-clipwin-body pre code { background: transparent; border: none; padding: 0; color: inherit; font-size: .92em; white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; }
.cnb-clipwin-body a { color: #0969da; text-decoration: none; }
.cnb-clipwin-body a:hover { text-decoration: underline; }
`);
} catch (_) {}
/* 剪贴板窗口滚动条样式:细黑条(仅作用于剪贴板窗口) */
try {
GM_addStyle(`
/* Firefox */
.cnb-clipwin, .cnb-clipwin-content, .cnb-clipwin-body, .cnb-clipwin-body pre {
scrollbar-width: thin;
scrollbar-color: #000 transparent;
}
/* WebKit */
.cnb-clipwin::-webkit-scrollbar,
.cnb-clipwin-content::-webkit-scrollbar,
.cnb-clipwin-body::-webkit-scrollbar,
.cnb-clipwin-body pre::-webkit-scrollbar {
width: 3px;
height: 3px;
}
.cnb-clipwin::-webkit-scrollbar-track,
.cnb-clipwin-content::-webkit-scrollbar-track,
.cnb-clipwin-body::-webkit-scrollbar-track,
.cnb-clipwin-body pre::-webkit-scrollbar-track {
background: transparent;
}
.cnb-clipwin::-webkit-scrollbar-thumb,
.cnb-clipwin-content::-webkit-scrollbar-thumb,
.cnb-clipwin-body::-webkit-scrollbar-thumb,
.cnb-clipwin-body pre::-webkit-scrollbar-thumb {
background: #000000;
border-radius: 2px;
}
.cnb-clipwin::-webkit-scrollbar-thumb:hover,
.cnb-clipwin-content::-webkit-scrollbar-thumb:hover,
.cnb-clipwin-body::-webkit-scrollbar-thumb:hover,
.cnb-clipwin-body pre::-webkit-scrollbar-thumb:hover {
background: #000000;
}
`);
} catch (_) {}
if (!CONFIG.repoPath || !CONFIG.accessToken) {
if (typeof GM_notification === 'function') {
GM_notification({ text: '请先在设置中配置仓库路径与访问令牌', title: 'CNB Issue工具', timeout: 3000 });
}
if (typeof openSettingsDialog === 'function') openSettingsDialog();
return;
}
// 仅创建窗口(无遮罩)
const dialog = document.createElement('div');
dialog.className = 'cnb-clipwin';
__CNB_CLIP_DIALOG = dialog;
dialog.innerHTML = `
`;
let pinned = false;
function cleanup() {
document.removeEventListener('mousedown', onDocDown, true);
document.removeEventListener('mouseup', onDocUp, true);
document.removeEventListener('mousemove', onDocMove, true);
document.removeEventListener('click', onOutsideClick, true);
}
function close() {
cleanup();
try { dialog.remove(); } catch (_) {}
try { if (__CNB_CLIP_DIALOG === dialog) __CNB_CLIP_DIALOG = null; } catch (_) {}
}
const btnPin = dialog.querySelector('.cnb-clipwin-pin');
if (btnPin) btnPin.addEventListener('click', () => {
pinned = !pinned;
const path = btnPin.querySelector('.menuBar-Btn_Icon-pin');
if (path) {
if (pinned) path.classList.add('isActive');
else path.classList.remove('isActive');
}
});
// 复制按钮:复制窗口内容(优先原始 Markdown)
const btnCopy = dialog.querySelector('.cnb-clipwin-copy');
if (btnCopy) btnCopy.addEventListener('click', async () => {
try {
const body = dialog.querySelector('#cnb-clipwin-body');
const text = (body && body.dataset && body.dataset.mdraw) ? body.dataset.mdraw : (body ? body.textContent : '');
if (typeof GM_setClipboard === 'function') {
GM_setClipboard(String(text || ''), 'text');
} else if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(String(text || ''));
}
if (typeof GM_notification === 'function') {
GM_notification({ text: '已复制到剪贴板', title: 'CNB 剪贴板', timeout: 2000 });
}
} catch (_) {}
});
// 点击窗口外关闭(未固定时)
function onOutsideClick(e) {
if (pinned) return;
if (!dialog.contains(e.target)) close();
}
document.addEventListener('click', onOutsideClick, true);
// 拖动逻辑(拖拽 header)
const header = dialog.querySelector('.cnb-clipwin-header');
let dragging = false;
let startX = 0, startY = 0, boxX = 0, boxY = 0;
function onDocDown(e) {
if (header && header.contains(e.target)) {
dragging = true;
const rect = dialog.getBoundingClientRect();
startX = e.clientX;
startY = e.clientY;
boxX = rect.left;
boxY = rect.top;
// 拖动时改为绝对定位并去掉居中 transform
dialog.style.left = rect.left + 'px';
dialog.style.top = rect.top + 'px';
dialog.style.transform = 'none';
}
}
function onDocUp() {
dragging = false;
}
function onDocMove(e) {
if (!dragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
dialog.style.left = (boxX + dx) + 'px';
dialog.style.top = (boxY + dy) + 'px';
}
document.addEventListener('mousedown', onDocDown, true);
document.addEventListener('mouseup', onDocUp, true);
document.addEventListener('mousemove', onDocMove, true);
document.body.appendChild(dialog);
const bodyEl = dialog.querySelector('#cnb-clipwin-body');
// 读取剪贴板位置(Issue编号)
let __clipIssueNum = '';
try {
if (typeof GM_getValue === 'function') {
const v = GM_getValue('cnbClipboardIssue', '');
__clipIssueNum = String(v || '').trim();
}
} catch (_) {}
if (!__clipIssueNum) {
if (bodyEl) bodyEl.textContent = '未配置剪贴板位置';
return;
}
const url = `${CONFIG.apiBase}/${CONFIG.repoPath}${CONFIG.issueEndpoint}/${encodeURIComponent(__clipIssueNum)}`;
GM_xmlhttpRequest({
method: 'GET',
url,
headers: {
'Accept': 'application/json',
'Authorization': `${CONFIG.accessToken}`
},
responseType: 'json',
onload: function(res) {
try {
if (res.status >= 200 && res.status < 300) {
let data = null;
try {
data = (typeof res.response === 'object' && res.response !== null)
? res.response
: JSON.parse(res.responseText || '{}');
} catch (_) {}
const rawBody = typeof data?.body === 'string' ? data.body : '';
const md = typeof cleanMarkdownContent === 'function'
? cleanMarkdownContent(String(rawBody || ''))
: String(rawBody || '');
if (bodyEl) {
bodyEl.dataset.mdraw = md;
bodyEl.innerHTML = (typeof markdownToHtml === 'function') ? markdownToHtml(md) : md;
// 移除所有
,避免用换行标签作为分隔
try { bodyEl.querySelectorAll('br').forEach(br => br.remove()); } catch (_) {}
// 设置标题文本(优先 Issue 标题)
const titleEl = dialog.querySelector('.cnb-clipwin-title');
const t = (data && typeof data.title === 'string' && data.title) ? data.title : '剪贴板';
if (titleEl) titleEl.textContent = t;
// 为每个代码块注入右上角复制按钮,并设置布局
const pres = bodyEl.querySelectorAll('pre');
pres.forEach(pre => {
try {
pre.style.marginTop = '5px';
pre.style.marginBottom = '5px';
pre.style.position = 'relative';
if (!pre.style.paddingRight) pre.style.paddingRight = '36px';
if (!pre.querySelector('.cnb-codecopy-inline')) {
// 确保右上角控制容器(同一父容器,便于排列“展开”与“复制”)
let controls = pre.querySelector('.cnb-code-controls');
if (!controls) {
controls = document.createElement('div');
controls.className = 'cnb-code-controls';
pre.appendChild(controls);
}
const btn = document.createElement('button');
btn.className = 'cnb-codecopy-inline';
btn.type = 'button';
btn.title = '复制代码';
btn.setAttribute('aria-label', '复制代码');
// 样式由 .cnb-code-controls 统一提供
btn.innerHTML = '';
btn.addEventListener('mouseenter', () => { btn.style.background = 'rgba(255,255,255,0.18)'; btn.style.opacity = '0.95'; });
btn.addEventListener('mouseleave', () => { btn.style.background = 'rgba(255,255,255,0.10)'; btn.style.opacity = ''; });
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const codeEl = pre.querySelector('code');
const text = codeEl ? codeEl.textContent : pre.textContent;
try {
if (typeof GM_setClipboard === 'function') {
GM_setClipboard(String(text || ''), 'text');
} else if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(String(text || ''));
}
if (typeof GM_notification === 'function') {
GM_notification({ text: '代码已复制', title: 'CNB 剪贴板', timeout: 1500 });
}
// 点击反馈动画:对勾+变色+轻微放大,随后恢复
try {
const prevHTML = btn.innerHTML;
const prevBg = btn.style.background;
const prevTransform = btn.style.transform;
// 增强过渡,加入缩放动画
const prevTransition = btn.style.transition;
btn.style.transition = prevTransition ? (prevTransition + ', transform .12s ease') : 'transform .12s ease, background-color .15s ease, opacity .15s ease';
// 切换对勾图标与高亮
btn.innerHTML = '';
btn.style.background = 'rgba(46,160,67,0.35)'; // 绿色反馈
btn.style.transform = 'scale(1.08)';
setTimeout(() => {
btn.style.transform = prevTransform || '';
btn.style.background = prevBg || 'rgba(255,255,255,0.10)';
btn.innerHTML = prevHTML;
// 恢复原 transition(若有)
if (prevTransition !== undefined) btn.style.transition = prevTransition;
}, 700);
} catch (_) {}
} catch (_) {}
});
controls.appendChild(btn);
}
} catch (_) {}
});
}
// 行为增强:为每个代码块提供“两行预览 + 展开/收起”,复制仍复制全文
try {
if (typeof GM_addStyle === 'function') {
addStyleOnce('clipwin-pre-controls', `
.cnb-pre-collapsed {
max-height: 3.2em; /* 约两行 */
overflow: hidden;
position: relative;
}
.cnb-pre-collapsed::after {
content: '';
position: absolute;
left: 0; right: 0; bottom: 0;
height: 28px;
background: linear-gradient(to bottom, rgba(11,16,33,0), rgba(11,16,33,0.85));
pointer-events: none;
}
/* 顶部右侧控制容器:右为复制,左为展开 */
.cnb-code-controls {
position: absolute;
top: 4px;
right: 6px;
display: inline-flex;
align-items: center;
gap: 6px;
}
.cnb-code-controls .cnb-codecopy-inline,
.cnb-code-controls .cnb-code-toggle {
border: none;
background: rgba(255,255,255,0.10);
color: #e6edf3;
padding: 4px 6px;
border-radius: 6px;
cursor: pointer;
line-height: 1;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
.cnb-code-controls .cnb-codecopy-inline:hover,
.cnb-code-controls .cnb-code-toggle:hover {
background: rgba(255,255,255,0.18);
}
`);
}
} catch (_) {}
try {
const pres2 = bodyEl ? bodyEl.querySelectorAll('pre') : [];
pres2.forEach(pre => {
try {
const codeEl = pre.querySelector('code') || pre;
const text = codeEl ? (codeEl.textContent || '') : (pre.textContent || '');
const lines = String(text || '').split('\n');
if (lines.length <= 2) return;
// 初始折叠为两行
pre.classList.add('cnb-pre-collapsed');
// 若不存在展开按钮则添加
/* 将展开按钮插入到右上角控制容器中,位于复制按钮左侧 */
// 确保控制容器存在
let __controls = pre.querySelector('.cnb-code-controls');
if (!__controls) {
__controls = document.createElement('div');
__controls.className = 'cnb-code-controls';
pre.appendChild(__controls);
}
if (!pre.querySelector('.cnb-code-toggle')) {
const tbtn = document.createElement('button');
tbtn.type = 'button';
tbtn.className = 'cnb-code-toggle';
tbtn.textContent = '展开';
tbtn.title = '展开/收起';
tbtn.addEventListener('click', (e) => {
e.stopPropagation();
const collapsed = pre.classList.toggle('cnb-pre-collapsed');
tbtn.textContent = collapsed ? '展开' : '收起';
});
const copyBtn = __controls.querySelector('.cnb-codecopy-inline');
if (copyBtn) __controls.insertBefore(tbtn, copyBtn);
else __controls.appendChild(tbtn);
}
} catch (_) {}
});
} catch (_) {}
// 基于 h2 构建章节按键并切换显示
try {
const contentDiv = dialog.querySelector('.cnb-clipwin-content');
if (contentDiv && bodyEl) {
const h2s = Array.from(bodyEl.querySelectorAll('h2'));
if (h2s.length) {
const sections = [];
for (let i = 0; i < h2s.length; i++) {
const h = h2s[i];
const sec = document.createElement('div');
sec.className = 'cnb-sec';
// 将 h2 及其之后内容(直到下一个 h2)打包进 section
h.parentNode.insertBefore(sec, h);
sec.appendChild(h);
let next = sec.nextSibling;
while (next && !(next.nodeType === 1 && next.tagName && next.tagName.toLowerCase() === 'h2')) {
const move = next;
next = next.nextSibling;
sec.appendChild(move);
}
sections.push(sec);
}
// 在正文之前插入 tabs 容器(注入样式)
try {
if (typeof GM_addStyle === 'function') {
GM_addStyle(`
.cnb-clipwin-tabs {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 6px 0 8px;
}
.cnb-tab {
appearance: none;
border: 1px solid #d0d7de;
border-radius: 9999px;
padding: 4px 10px;
background: #fff;
color: #24292f;
cursor: pointer;
font-size: 12px;
line-height: 1.2;
transition: background-color .15s ease, color .15s ease, border-color .15s ease, box-shadow .15s ease;
}
.cnb-tab:hover {
background: #f6f8fa;
border-color: #b9c2cc;
}
.cnb-tab.active {
background: #0366d6;
color: #fff;
border-color: #0256b9;
box-shadow: 0 1px 2px rgba(2, 86, 185, .15);
}
.cnb-tab:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(3,102,214,.25);
}
`);
}
} catch (_) {}
let tabs = contentDiv.querySelector('.cnb-clipwin-tabs');
if (!tabs) {
tabs = document.createElement('div');
tabs.className = 'cnb-clipwin-tabs';
contentDiv.insertBefore(tabs, bodyEl);
} else {
tabs.innerHTML = '';
}
h2s.forEach((h, idx) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = (h.textContent || '').trim() || ('Section ' + (idx + 1));
btn.className = 'cnb-tab';
btn.addEventListener('click', () => {
sections.forEach((s, j) => { s.style.display = (j === idx) ? '' : 'none'; });
Array.from(tabs.children).forEach((b, i) => {
b.classList.toggle('active', i === idx);
});
});
tabs.appendChild(btn);
});
// 默认激活第一个
if (tabs.firstElementChild) tabs.firstElementChild.click();
}
}
} catch (_) {}
// 渲染后隐藏 h2,并收紧 pre 间距与清理多余换行
try {
if (bodyEl) {
// 隐藏所有 h2,去除占位
bodyEl.querySelectorAll('h2').forEach(h => {
h.style.display = 'none';
h.style.margin = '0';
h.style.padding = '0';
});
// 移除所有
与空段落,避免形成额外间隙
bodyEl.querySelectorAll('br').forEach(br => br.remove());
bodyEl.querySelectorAll('p').forEach(p => {
const txt = (p.textContent || '').trim();
const onlyBr = p.children.length === 1 && p.firstElementChild && p.firstElementChild.tagName.toLowerCase() === 'br';
if (!txt || onlyBr) p.remove();
});
// 统一设置代码块上下间距与内边距
bodyEl.querySelectorAll('pre').forEach(pre => {
pre.style.marginTop = '-17.5px';
pre.style.marginBottom = '2.5px';
pre.style.paddingTop = '6px';
pre.style.paddingBottom = '6px';
});
// 额外收紧:取消段落与分段容器的默认间距
bodyEl.querySelectorAll('p').forEach(p => {
p.style.marginTop = '0';
p.style.marginBottom = '0';
});
bodyEl.querySelectorAll('.cnb-sec').forEach(sec => {
sec.style.margin = '0';
sec.style.padding = '0';
});
}
} catch (_) {}
} else {
if (bodyEl) bodyEl.textContent = `加载失败: HTTP ${res.status}`;
}
} catch (_) {
if (bodyEl) bodyEl.textContent = '解析失败';
}
},
onerror: function() {
if (bodyEl) bodyEl.textContent = '网络请求失败,请稍后重试';
}
});
}
// 创建Issue
function createIssue(title, content, labels = [], callback) {
if (!CONFIG.repoPath || !CONFIG.accessToken) {
if (typeof GM_notification === 'function') {
GM_notification({ text: '请先在设置中配置仓库路径与访问令牌', title: 'CNB Issue工具', timeout: 3000 });
}
if (typeof openSettingsDialog === 'function') openSettingsDialog();
if (typeof callback === 'function') callback(false);
return;
}
const issueData = {
repoId: CONFIG.repoPath,
title: title,
body: content,
labels: labels,
assignees: []
};
const apiUrl = `${CONFIG.apiBase}/${CONFIG.repoPath}${CONFIG.issueEndpoint}`;
GM_xmlhttpRequest({
method: 'POST',
url: apiUrl,
headers: {
'Content-Type': 'application/json',
'Authorization': `${CONFIG.accessToken}`,
'Accept': 'application/json'
},
data: JSON.stringify(issueData),
responseType: 'json',
onload: function(response) {
if (response.status === 200 || response.status === 201) {
// 解析返回,取 issueId(兼容不同字段)
let respObj = null;
try {
respObj = typeof response.response === 'object' && response.response !== null
? response.response
: JSON.parse(response.responseText || '{}');
} catch (_) {
respObj = null;
}
const issueId = respObj?.id ?? respObj?.number ?? respObj?.iid ?? respObj?.issue_id;
const notifySuccess = () => {
GM_notification({
text: `Issue创建成功!`,
title: 'CNB Issue工具',
timeout: 3000
});
if (callback) callback(true, issueId);
};
// 若有标签,则单独 PUT 标签
if (Array.isArray(labels) && labels.length > 0 && issueId != null) {
const labelsUrl = `${CONFIG.apiBase}/${CONFIG.repoPath}${CONFIG.issueEndpoint}/${issueId}/labels`;
GM_xmlhttpRequest({
method: 'PUT',
url: labelsUrl,
headers: {
'Content-Type': 'application/json',
'Authorization': `${CONFIG.accessToken}`,
'Accept': 'application/json'
},
data: JSON.stringify({ labels }),
responseType: 'json',
onload: function(res2) {
if (res2.status >= 200 && res2.status < 300) {
notifySuccess();
} else {
let msg = `HTTP ${res2.status}`;
try {
const err = typeof res2.response === 'string'
? JSON.parse(res2.response) : res2.response;
if (err?.message) msg = err.message;
} catch (_) {}
GM_notification({
text: `Issue已创建,但设置标签失败:${msg}`,
title: 'CNB Issue工具',
timeout: 5000
});
if (callback) callback(true, issueId);
}
},
onerror: function() {
GM_notification({
text: `Issue已创建,但设置标签时网络错误`,
title: 'CNB Issue工具',
timeout: 5000
});
if (callback) callback(true, issueId);
}
});
} else {
// 无标签或无法解析 issueId,直接成功
notifySuccess();
}
} else {
let errorMsg = `HTTP ${response.status}`;
try {
const errorData = typeof response.response === 'string' ?
JSON.parse(response.response) : response.response;
if (errorData && errorData.message) {
errorMsg = errorData.message;
}
} catch (e) {}
GM_notification({
text: `创建失败: ${errorMsg}`,
title: 'CNB Issue工具',
timeout: 5000
});
if (callback) callback(false);
}
},
onerror: function(error) {
GM_notification({
text: `网络请求失败`,
title: 'CNB Issue工具',
timeout: 5000
});
if (callback) callback(false);
}
});
}
// 关闭Issue(设置为 closed,state_reason=completed)
function closeIssue(issueId, stateReason = 'completed', callback) {
if (issueId == null) {
if (typeof callback === 'function') callback(false);
return;
}
const url = `${CONFIG.apiBase}/${CONFIG.repoPath}${CONFIG.issueEndpoint}/${issueId}`;
GM_xmlhttpRequest({
method: 'PATCH',
url,
headers: {
'Content-Type': 'application/json',
'Authorization': `${CONFIG.accessToken}`,
'Accept': 'application/json'
},
data: JSON.stringify({
state: 'closed',
state_reason: stateReason
}),
responseType: 'json',
onload: function(res) {
if (res.status >= 200 && res.status < 300) {
if (typeof callback === 'function') callback(true);
} else {
let msg = `HTTP ${res.status}`;
try {
const err = typeof res.response === 'string' ? JSON.parse(res.response) : res.response;
if (err?.message) msg = err.message;
} catch (_) {}
if (typeof GM_notification === 'function') {
GM_notification({
text: `标记完成失败:${msg}`,
title: 'CNB Issue工具',
timeout: 5000
});
}
if (typeof callback === 'function') callback(false);
}
},
onerror: function() {
if (typeof GM_notification === 'function') {
GM_notification({
text: `网络请求失败(关闭Issue)`,
title: 'CNB Issue工具',
timeout: 5000
});
}
if (typeof callback === 'function') callback(false);
}
});
}
// 直达目标解码:获取 cnb.cool /goto?url= 的目标地址
function getCnbGotoTarget(urlLike) {
try {
const u = new URL(urlLike, location.href);
const raw = u.searchParams.get('url') || '';
if (!raw) return '';
// 解码 1-2 次,兼容已编码/双重编码
let t = decodeURIComponent(raw);
try {
// 如果仍是百分号编码痕迹,再解一次
if (/%[0-9A-Fa-f]{2}/.test(t)) t = decodeURIComponent(t);
} catch (_) {}
// 只允许 http/https
if (!/^https?:\/\//i.test(t)) return '';
return t;
} catch (_) {
return '';
}
}
// 若当前位于 cnb.cool 的 /goto 跳转页,立即重定向到真实目标
function handleCnbGotoPage() {
const isCNB = /\b(^|\.)cnb\.cool$/i.test(location.hostname);
if (!isCNB) return;
if (location.pathname === '/goto') {
const target = getCnbGotoTarget(location.href);
if (target) {
// 不留历史记录
location.replace(target);
}
}
}
// 将页面内所有 /goto?url= 链接批量改写为直链
function rewriteCnbGotoLinks(root = document) {
try {
const isCNB = /\b(^|\.)cnb\.cool$/i.test(location.hostname);
if (!isCNB) return;
const list = root.querySelectorAll('a[href*="/goto?url="], a[href^="/goto?url="], a[href^="https://cnb.cool/goto?url="]');
list.forEach(a => {
const t = getCnbGotoTarget(a.href);
if (t) a.href = t;
});
} catch (_) {}
}
// 事件委托兜底:拦截点击 /goto?url= 的链接并直接打开目标
function cnbGotoClickHandler(e) {
const isCNB = /\b(^|\.)cnb\.cool$/i.test(location.hostname);
if (!isCNB) return;
// 仅关心主按钮/中键点击到
let el = e.target;
while (el && el !== document && !(el instanceof HTMLAnchorElement)) {
el = el.parentElement;
}
if (!(el instanceof HTMLAnchorElement)) return;
const href = el.getAttribute('href') || '';
// 使用绝对地址判断,以覆盖相对路径
const abs = (new URL(href, location.href)).href;
if (!/\/goto\?url=/i.test(abs)) return;
const target = getCnbGotoTarget(abs);
if (!target) return;
// 阻止站内跳转页
e.preventDefault();
e.stopPropagation();
// 兼容中键或带修饰键的新开方式
const newTab = e.button === 1 || e.ctrlKey || e.metaKey;
if (newTab) {
window.open(target, '_blank', 'noopener');
} else {
location.href = target;
}
}
// 初始化
function init() {
// 读取持久化配置
try {
if (typeof GM_getValue === 'function') {
const repo = GM_getValue('repoPath', CONFIG.repoPath);
const token = GM_getValue('accessToken', CONFIG.accessToken);
CONFIG.repoPath = repo || CONFIG.repoPath;
CONFIG.accessToken = token || CONFIG.accessToken;
const tags = GM_getValue('cnbTags', []);
SAVED_TAGS = Array.isArray(tags) ? tags : [];
const hk = GM_getValue('cnbHotkey', START_HOTKEY);
if (hk) START_HOTKEY = normalizeHotkeyString(hk);
const hkEnabled = GM_getValue('cnbHotkeyEnabled', HOTKEY_ENABLED);
HOTKEY_ENABLED = !!hkEnabled;
}
} catch (_) {}
createFloatingButton();
// cnb.cool 跳转页直达与站内直链化
handleCnbGotoPage();
if (/\b(^|\.)cnb\.cool$/i.test(location.hostname)) {
// 首次批量改写
rewriteCnbGotoLinks(document);
// 事件委托拦截兜底
document.addEventListener('click', cnbGotoClickHandler, true);
// 监听后续动态内容
try {
__CNB_MO = new MutationObserver(mutations => {
for (const m of mutations) {
m.addedNodes && m.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
rewriteCnbGotoLinks(node);
}
});
}
});
__CNB_MO.observe(document.documentElement || document.body, { childList: true, subtree: true });
try {
if (!__CNB_UNLOAD_BOUND) {
window.addEventListener('beforeunload', () => {
try { if (__CNB_MO) { __CNB_MO.disconnect(); __CNB_MO = null; } } catch (_) {}
try { if (__CNB_CLIP_DIALOG && __CNB_CLIP_DIALOG.parentNode) { __CNB_CLIP_DIALOG.remove(); __CNB_CLIP_DIALOG = null; } } catch (_) {}
}, { once: true });
__CNB_UNLOAD_BOUND = true;
}
} catch (_) {}
} catch (_) {}
}
// 注册全局快捷键
document.addEventListener('keydown', globalHotkeyHandler, true);
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();