const annotationsSummaryModal = document.getElementById('annotations-summary-modal');
const annotationsSummaryCloseBtn = document.getElementById('annotations-summary-close-btn');
const annotationsFilterTypeSelect = document.getElementById('annotations-filter-type');
const annotationsFilterContentSelect = document.getElementById('annotations-filter-content');
const annotationsSummaryTableBody = document.getElementById('annotations-summary-table-body');
const annotationsSummaryColorFilter = document.getElementById('annotations-summary-color-filter');
// 获取所有出现过的高亮颜色
function getAllHighlightColors() {
if (!window.data || !window.data.annotations) return [];
const colorSet = new Set();
window.data.annotations.forEach(ann => {
if (ann.highlightColor) colorSet.add(ann.highlightColor);
});
return Array.from(colorSet);
}
// 渲染颜色多选区
function renderColorFilter(selectedColors) {
const allColors = getAllHighlightColors();
annotationsSummaryColorFilter.innerHTML = '';
if (allColors.length === 0) return;
allColors.forEach(color => {
const label = document.createElement('label');
label.className = 'annotations-summary-color-checkbox';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = color;
checkbox.checked = selectedColors.includes(color);
checkbox.addEventListener('change', () => {
// 重新渲染表格
const checkedColors = Array.from(annotationsSummaryColorFilter.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value);
populateAnnotationsSummaryTable(
annotationsFilterTypeSelect.value,
annotationsFilterContentSelect.value,
checkedColors
);
});
const swatch = document.createElement('span');
swatch.className = 'annotations-summary-color-swatch';
swatch.style.backgroundColor = typeof getHighlightColor === 'function' ? getHighlightColor(color) : color;
label.appendChild(checkbox);
label.appendChild(swatch);
label.appendChild(document.createTextNode(' '));
annotationsSummaryColorFilter.appendChild(label);
});
}
// 主表格渲染函数,增加颜色筛选参数
function populateAnnotationsSummaryTable(typeFilter = 'all', contentFilter = 'all', colorFilter = null) {
console.log('[调试] populateAnnotationsSummaryTable called, data:', window.data, 'tocStructure:', (typeof window.getCurrentTocStructure === 'function') ? window.getCurrentTocStructure() : null);
if (!window.data || !window.data.annotations || !annotationsSummaryTableBody) {
annotationsSummaryTableBody.innerHTML = '
| 暂无数据或批注未加载。 |
';
return;
}
// 默认全选所有颜色
let allColors = getAllHighlightColors();
if (!colorFilter) colorFilter = allColors;
renderColorFilter(colorFilter);
// 清空表格内容,防止重复
annotationsSummaryTableBody.innerHTML = '';
// 对批注数据进行去重处理
// 创建一个 Map 用于检测重复的批注
const uniqueAnnotations = new Map();
// 遍历所有批注,按照目标标识符和内容类型进行去重
window.data.annotations.forEach(ann => {
// 获取批注的唯一标识
let targetSelector = ann.target && ann.target.selector && ann.target.selector[0];
let uniqueKey = '';
if (targetSelector) {
// 使用 subBlockId 或 blockIndex 作为唯一标识的一部分
if (targetSelector.subBlockId) {
uniqueKey = `${ann.targetType}_subBlock_${targetSelector.subBlockId}`;
} else if (targetSelector.blockIndex !== undefined) {
uniqueKey = `${ann.targetType}_block_${targetSelector.blockIndex}`;
}
}
// 如果有有效的唯一标识,则添加到 Map 中
if (uniqueKey) {
// 如果是相同位置的批注,保留最新的(假设 id 越大越新)
if (!uniqueAnnotations.has(uniqueKey) ||
(ann.id && uniqueAnnotations.get(uniqueKey).id && ann.id > uniqueAnnotations.get(uniqueKey).id)) {
uniqueAnnotations.set(uniqueKey, ann);
}
} else {
// 对于没有有效唯一标识的批注,使用 id 作为键
uniqueAnnotations.set(ann.id || _page_generateUUID(), ann);
}
});
// 将去重后的批注转换为数组并应用筛选条件
const filteredAnnotations = Array.from(uniqueAnnotations.values()).filter(ann => {
const typeMatch = typeFilter === 'all' || ann.motivation === typeFilter;
const contentMatch = contentFilter === 'all' || ann.targetType === contentFilter;
const colorMatch = !ann.highlightColor || colorFilter.includes(ann.highlightColor);
return typeMatch && contentMatch && colorMatch;
});
// 按照位置排序(先按内容类型,再按块索引或子块ID)
filteredAnnotations.sort((a, b) => {
// 先按内容类型排序
if (a.targetType !== b.targetType) {
return a.targetType === 'ocr' ? -1 : 1;
}
// 再按块索引或子块ID排序
const aSel = a.target && a.target.selector && a.target.selector[0];
const bSel = b.target && b.target.selector && b.target.selector[0];
if (aSel && bSel) {
// 如果都有 blockIndex,按 blockIndex 排序
if (aSel.blockIndex !== undefined && bSel.blockIndex !== undefined) {
return aSel.blockIndex - bSel.blockIndex;
}
// 如果都有 subBlockId,按 subBlockId 排序
if (aSel.subBlockId && bSel.subBlockId) {
const aIds = aSel.subBlockId.split('.');
const bIds = bSel.subBlockId.split('.');
// 先比较主块索引
const aMainBlock = parseInt(aIds[0]);
const bMainBlock = parseInt(bIds[0]);
if (aMainBlock !== bMainBlock) {
return aMainBlock - bMainBlock;
}
// 再比较子块索引
if (aIds.length > 1 && bIds.length > 1) {
return parseInt(aIds[1]) - parseInt(bIds[1]);
}
}
}
// 默认按 id 排序(如果有)
return (a.id || '') < (b.id || '') ? -1 : 1;
});
// ========== TOC 分组渲染 ===========
let tocStructure = (typeof window.getCurrentTocStructure === 'function') ? window.getCurrentTocStructure() : null;
let grouped = {};
let ungrouped = [];
// 辅助:递归查找批注属于哪个 TOC 节点,并返回完整路径
function findTocPathForAnnotation(tocNodes, blockIndex, subBlockId, path = []) {
let found = null;
for (let node of tocNodes) {
// 判断 blockIndex 是否在本节点范围内
let inRange = false;
if (blockIndex !== null && node.startBlockIndex !== undefined) {
if (node.endBlockIndex !== null && node.endBlockIndex !== undefined) {
inRange = blockIndex >= node.startBlockIndex && blockIndex <= node.endBlockIndex;
} else {
inRange = blockIndex >= node.startBlockIndex;
}
}
// 判断 subBlockId 是否属于本节点(可扩展)
// 这里只做 blockIndex 匹配,subBlockId 可按需扩展
if (inRange) {
// 递归查找更小的子节点
let childFound = null;
if (node.children && node.children.length > 0) {
childFound = findTocPathForAnnotation(node.children, blockIndex, subBlockId, path.concat([node]));
}
if (childFound) return childFound;
return path.concat([node]);
}
}
return null;
}
if (tocStructure && tocStructure.children && tocStructure.children.length > 0) {
console.log('[批注分组] TOC结构节点数:', tocStructure.children.length);
filteredAnnotations.forEach(ann => {
let targetSelector = ann.target && ann.target.selector && ann.target.selector[0];
let blockIndex = targetSelector && targetSelector.blockIndex !== undefined ? parseInt(targetSelector.blockIndex, 10) : null;
let subBlockId = targetSelector && targetSelector.subBlockId ? String(targetSelector.subBlockId) : null;
// 新增:如果 blockIndex 为空但 subBlockId 存在,自动用 subBlockId 的前半部分
if (blockIndex === null && subBlockId) {
const parts = subBlockId.split('.');
if (parts.length > 0 && !isNaN(parseInt(parts[0], 10))) {
blockIndex = parseInt(parts[0], 10);
}
}
let tocPath = findTocPathForAnnotation(tocStructure.children, blockIndex, subBlockId);
if (tocPath && tocPath.length > 0) {
let groupKey = tocPath.map(n => n.text).join(' > ');
if (!grouped[groupKey]) grouped[groupKey] = { path: tocPath, items: [] };
grouped[groupKey].items.push(ann);
console.log(`[批注分组] blockIndex: ${blockIndex}, subBlockId: ${subBlockId}, 分组到: ${groupKey}`);
} else {
ungrouped.push(ann);
console.log(`[批注分组] blockIndex: ${blockIndex}, subBlockId: ${subBlockId}, 未分组`);
}
});
} else {
ungrouped = filteredAnnotations;
}
// ========== 渲染分组 ===========
let groupCount = 0;
for (let key in grouped) {
if (!grouped[key].items.length) continue;
groupCount += grouped[key].items.length;
const groupRow = document.createElement('tr');
groupRow.className = 'toc-group';
// XSS 防护:使用 textContent 而不是 innerHTML
// key 来自文档标题,可能包含恶意 HTML
const td = document.createElement('td');
td.setAttribute('colspan', '7');
td.textContent = `${key}(${grouped[key].items.length})`;
groupRow.appendChild(td);
annotationsSummaryTableBody.appendChild(groupRow);
grouped[key].items.forEach(ann => {
const row = annotationsSummaryTableBody.insertRow();
row.insertCell().textContent = ann.motivation === 'commenting' ? '批注' : (ann.motivation === 'highlighting' ? '高亮' : ann.motivation);
row.insertCell().textContent = ann.targetType ? ann.targetType.toUpperCase() : 'N/A';
let identifierText = 'N/A';
let targetSelector = ann.target && ann.target.selector && ann.target.selector[0];
if (targetSelector) {
if (targetSelector.subBlockId) {
identifierText = `子块: ${targetSelector.subBlockId}`;
} else if (targetSelector.blockIndex !== undefined) {
identifierText = `块: ${targetSelector.blockIndex}`;
}
}
row.insertCell().textContent = identifierText;
// 增强文本片段预览(优先原始 Markdown,再回退 exact)
const textCell = row.insertCell();
let textSnippet = '[无文本片段]';
if (targetSelector) {
// 优先:以所属块的原始 Markdown 作为预览
let preferredBlockIndex = null;
if (targetSelector.blockIndex !== undefined && targetSelector.blockIndex !== null) {
preferredBlockIndex = parseInt(targetSelector.blockIndex, 10);
} else if (targetSelector.subBlockId) {
const parts = String(targetSelector.subBlockId).split('.');
if (parts.length > 0 && !isNaN(parseInt(parts[0], 10))) preferredBlockIndex = parseInt(parts[0], 10);
}
if (preferredBlockIndex !== null &&
window.currentBlockTokensForCopy &&
window.currentBlockTokensForCopy[ann.targetType] &&
window.currentBlockTokensForCopy[ann.targetType][preferredBlockIndex] &&
typeof window.currentBlockTokensForCopy[ann.targetType][preferredBlockIndex].raw === 'string') {
textSnippet = window.currentBlockTokensForCopy[ann.targetType][preferredBlockIndex].raw;
} else if (targetSelector.exact) {
// 回退:展示 exact(渲染后的文本片段)
textSnippet = targetSelector.exact;
}
}
// 限制显示长度,但保留更多内容
if (textSnippet && textSnippet.length > 150) {
textCell.textContent = textSnippet.substring(0, 150) + '...';
textCell.title = textSnippet; // 鼠标悬停时显示完整内容
} else {
textCell.textContent = textSnippet || '[无文本片段]';
}
// 添加查看完整内容的功能
if (textSnippet && textSnippet.length > 50) {
textCell.style.cursor = 'pointer';
textCell.addEventListener('click', function() {
const fullTextDialog = document.createElement('div');
fullTextDialog.className = 'full-text-dialog';
fullTextDialog.style.position = 'fixed';
fullTextDialog.style.top = '50%';
fullTextDialog.style.left = '50%';
fullTextDialog.style.transform = 'translate(-50%, -50%)';
fullTextDialog.style.maxWidth = '80%';
fullTextDialog.style.maxHeight = '80%';
fullTextDialog.style.backgroundColor = 'white';
fullTextDialog.style.padding = '20px';
fullTextDialog.style.borderRadius = '8px';
fullTextDialog.style.boxShadow = '0 0 20px rgba(0,0,0,0.3)';
fullTextDialog.style.zIndex = '10000';
fullTextDialog.style.overflow = 'auto';
const closeBtn = document.createElement('button');
closeBtn.textContent = '关闭';
closeBtn.style.position = 'absolute';
closeBtn.style.top = '10px';
closeBtn.style.right = '10px';
closeBtn.style.padding = '5px 10px';
closeBtn.style.cursor = 'pointer';
const content = document.createElement('pre');
content.style.whiteSpace = 'pre-wrap';
content.style.wordBreak = 'break-word';
content.style.margin = '10px 0';
content.style.padding = '10px';
content.style.backgroundColor = '#f5f5f5';
content.style.border = '1px solid #ddd';
content.style.borderRadius = '4px';
content.textContent = textSnippet;
fullTextDialog.appendChild(closeBtn);
fullTextDialog.appendChild(content);
document.body.appendChild(fullTextDialog);
closeBtn.onclick = function() {
if (fullTextDialog.parentNode === document.body) {
document.body.removeChild(fullTextDialog);
}
};
// 点击对话框外部关闭
document.addEventListener('click', function closeDialog(e) {
if (!fullTextDialog.contains(e.target) && e.target !== textCell) {
// 添加检查确保对话框仍然存在于文档中
if (fullTextDialog.parentNode === document.body) {
document.body.removeChild(fullTextDialog);
}
document.removeEventListener('click', closeDialog);
}
});
});
}
// ======= 可编辑笔记列 =======
const noteCell = row.insertCell();
const noteText = ann.body && ann.body.length > 0 && ann.body[0].value ? ann.body[0].value : '';
noteCell.textContent = noteText;
// 带批注类型 或 仅高亮类型都可编辑/添加
if (ann.motivation === 'commenting' || ann.motivation === 'highlighting') {
noteCell.style.cursor = 'pointer';
noteCell.title = noteText ? '点击编辑批注' : '点击添加批注';
if (!noteText && ann.motivation === 'highlighting') {
noteCell.style.color = '#aaa';
noteCell.textContent = '点击添加批注';
}
noteCell.addEventListener('click', function onEditNoteCell(e) {
if (noteCell.querySelector('textarea')) return; // 已经在编辑
const textarea = document.createElement('textarea');
textarea.value = noteText;
textarea.style.width = '98%';
textarea.style.minHeight = '40px';
textarea.style.fontSize = '13px';
textarea.style.fontFamily = 'inherit';
textarea.style.resize = 'vertical';
textarea.autofocus = true;
noteCell.innerHTML = '';
noteCell.appendChild(textarea);
textarea.focus();
// 保存函数
const saveNote = async () => {
const newVal = textarea.value.trim();
if (newVal === noteText || (!newVal && !noteText)) {
noteCell.textContent = noteText || '点击添加批注';
if (!noteText && ann.motivation === 'highlighting') noteCell.style.color = '#aaa';
return;
}
if (!newVal) {
noteCell.textContent = '内容不能为空';
setTimeout(() => { noteCell.textContent = noteText || '点击添加批注'; if (!noteText && ann.motivation === 'highlighting') noteCell.style.color = '#aaa'; }, 1200);
return;
}
noteCell.textContent = '保存中...';
try {
ann.body = [{ type: 'TextualBody', value: newVal, format: 'text/plain', purpose: 'commenting' }];
ann.modified = new Date().toISOString();
ann.motivation = 'commenting'; // 升级为批注
// 检查 updateAnnotationInDB 函数是否可用
if (typeof updateAnnotationInDB !== 'function') {
console.error('updateAnnotationInDB 函数未定义,请确保 storage.js 已正确加载');
throw new Error('保存批注的函数未定义');
}
await updateAnnotationInDB(ann);
// 更新window.data.annotations
const idx = window.data.annotations.findIndex(a => a.id === ann.id);
if (idx > -1) window.data.annotations[idx] = { ...ann };
// 刷新表格,保持筛选
const checkedColors = Array.from(annotationsSummaryColorFilter.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value);
populateAnnotationsSummaryTable(annotationsFilterTypeSelect.value, annotationsFilterContentSelect.value, checkedColors);
} catch (err) {
console.error('保存批注失败:', err);
noteCell.textContent = '[保存失败]';
setTimeout(() => { noteCell.textContent = noteText || '点击添加批注'; if (!noteText && ann.motivation === 'highlighting') noteCell.style.color = '#aaa'; }, 1500);
}
};
textarea.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
saveNote();
} else if (e.key === 'Escape') {
noteCell.textContent = noteText || '点击添加批注';
if (!noteText && ann.motivation === 'highlighting') noteCell.style.color = '#aaa';
}
});
textarea.addEventListener('blur', saveNote);
});
}
// ======= 可编辑颜色列 =======
const colorCell = row.insertCell();
if (ann.highlightColor) {
const swatch = document.createElement('span');
swatch.className = 'color-swatch';
swatch.style.backgroundColor = typeof getHighlightColor === 'function' ? getHighlightColor(ann.highlightColor) : ann.highlightColor;
swatch.style.cursor = 'pointer';
swatch.title = '点击更改高亮颜色';
colorCell.appendChild(swatch);
// 点击弹出下拉
swatch.addEventListener('click', function onEditColor(e) {
if (colorCell.querySelector('select')) return;
// 颜色选项:常用色+所有已用色
const commonColors = ['yellow','pink','lightblue','lightgreen','purple','orange','red','cyan','blue','green'];
const allColors = Array.from(new Set([...commonColors, ...getAllHighlightColors()]));
const select = document.createElement('select');
select.style.marginLeft = '6px';
allColors.forEach(c => {
const opt = document.createElement('option');
opt.value = c;
opt.textContent = c;
opt.style.backgroundColor = typeof getHighlightColor === 'function' ? getHighlightColor(c) : c;
if (c === ann.highlightColor) opt.selected = true;
select.appendChild(opt);
});
colorCell.appendChild(select);
select.focus();
// 保存颜色
const saveColor = async () => {
const newColor = select.value;
if (newColor === ann.highlightColor) {
colorCell.innerHTML = '';
colorCell.appendChild(swatch);
return;
}
colorCell.textContent = '保存中...';
try {
ann.highlightColor = newColor;
ann.modified = new Date().toISOString();
if (ann.motivation !== 'commenting') ann.motivation = 'highlighting';
// 检查 updateAnnotationInDB 函数是否可用
if (typeof updateAnnotationInDB !== 'function') {
console.error('updateAnnotationInDB 函数未定义,请确保 storage.js 已正确加载');
throw new Error('保存批注颜色的函数未定义');
}
await updateAnnotationInDB(ann);
// 更新window.data.annotations
const idx = window.data.annotations.findIndex(a => a.id === ann.id);
if (idx > -1) window.data.annotations[idx] = { ...ann };
// 刷新表格,保持筛选
const checkedColors = Array.from(annotationsSummaryColorFilter.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value);
populateAnnotationsSummaryTable(annotationsFilterTypeSelect.value, annotationsFilterContentSelect.value, checkedColors);
} catch (err) {
console.error('保存批注颜色失败:', err);
colorCell.textContent = '[保存失败]';
setTimeout(() => { colorCell.innerHTML = ''; colorCell.appendChild(swatch); }, 1500);
}
};
select.addEventListener('change', saveColor);
select.addEventListener('blur', saveColor);
});
} else {
colorCell.textContent = '-';
}
const actionsCell = row.insertCell();
const jumpButton = document.createElement('button');
jumpButton.textContent = '跳转';
jumpButton.className = 'action-btn';
jumpButton.dataset.annotationId = ann.id;
jumpButton.dataset.targetType = ann.targetType;
// 为跨子块批注提供回退子块ID与块索引
let dataBlockIndex = (targetSelector && targetSelector.blockIndex !== undefined) ? String(targetSelector.blockIndex) : '';
let dataSubBlockId = (targetSelector && targetSelector.subBlockId !== undefined) ? String(targetSelector.subBlockId) : '';
if (!dataSubBlockId && targetSelector && targetSelector.type === 'CrossBlockRangeSelector') {
if (Array.isArray(targetSelector.affectedSubBlocks) && targetSelector.affectedSubBlocks.length > 0) {
dataSubBlockId = String(targetSelector.affectedSubBlocks[0]);
} else if (targetSelector.startSubBlockId) {
dataSubBlockId = String(targetSelector.startSubBlockId);
}
}
if (!dataBlockIndex && dataSubBlockId) {
const parts = dataSubBlockId.split('.');
if (parts.length > 0 && parts[0]) dataBlockIndex = parts[0];
}
jumpButton.dataset.blockIndex = dataBlockIndex;
jumpButton.dataset.subBlockId = dataSubBlockId;
// 仅在分块对比模式禁用跳转;其余场景即使缺少定位信息也尝试按 annotationId 跳转
if (window.currentVisibleTabId === 'chunk-compare') {
jumpButton.disabled = true;
jumpButton.title = '分块对比模式下禁用跳转';
}
jumpButton.onclick = async function() {
closeAnnotationsSummaryModal(); // Close modal before jumping
const targetType = this.dataset.targetType;
const blockIndex = this.dataset.blockIndex;
const subBlockId = this.dataset.subBlockId;
if (!targetType) return;
// Switch tab if necessary
if (window.currentVisibleTabId !== targetType && window.currentVisibleTabId !== `${targetType}-content-wrapper`) { // Check against common tab ID patterns
if (typeof showTab === 'function') {
console.log(`Switching to tab: ${targetType} for jump action.`);
await Promise.resolve(showTab(targetType)); // Ensure tab switch completes
// Wait a brief moment for DOM updates after tab switch, if showTab involves async rendering.
await new Promise(resolve => setTimeout(resolve, 200));
} else {
alert('无法切换标签页。');
return;
}
}
// 优先:等待目标高亮/元素出现后再跳转(处理分批渲染/分块延时)
if (typeof window.scrollToAnnotationAsync === 'function') {
const okAsync = await window.scrollToAnnotationAsync(this.dataset.annotationId, {
targetType,
subBlockId,
blockIndex,
timeoutMs: 5000,
pollIntervalMs: 120
});
if (okAsync) return;
} else if (typeof window.scrollToAnnotation === 'function') {
const ok = window.scrollToAnnotation(this.dataset.annotationId, true);
if (ok) return;
}
// 回退:按 subBlockId 或 blockIndex 定位
let elementToJump = null;
const contentWrapperId = `${targetType}-content-wrapper`;
const contentWrapper = document.getElementById(contentWrapperId);
if (contentWrapper) {
if (subBlockId && subBlockId !== 'undefined' && subBlockId !== '') {
elementToJump = contentWrapper.querySelector(`.sub-block[data-sub-block-id="${subBlockId}"]`);
} else if (blockIndex !== '') {
elementToJump = contentWrapper.querySelector(`[data-block-index="${blockIndex}"]`);
}
}
if (elementToJump) {
elementToJump.scrollIntoView({ behavior: 'smooth', block: 'center' });
elementToJump.classList.add('jump-to-highlight-effect');
setTimeout(() => elementToJump.classList.remove('jump-to-highlight-effect'), 2500);
} else {
alert('在当前视图中未找到目标元素。它可能已被过滤或不存在。');
console.warn('Element not found for jump:', {targetType, blockIndex, subBlockId});
}
};
actionsCell.appendChild(jumpButton);
});
}
if (ungrouped.length) {
const groupRow = document.createElement('tr');
groupRow.className = 'toc-group';
// XSS 防护:使用 textContent(虽然 length 是数字,但保持一致性)
const td = document.createElement('td');
td.setAttribute('colspan', '7');
td.textContent = `未分组(${ungrouped.length})`;
groupRow.appendChild(td);
annotationsSummaryTableBody.appendChild(groupRow);
ungrouped.forEach(ann => {
const row = annotationsSummaryTableBody.insertRow();
row.insertCell().textContent = ann.motivation === 'commenting' ? '批注' : (ann.motivation === 'highlighting' ? '高亮' : ann.motivation);
row.insertCell().textContent = ann.targetType ? ann.targetType.toUpperCase() : 'N/A';
let identifierText = 'N/A';
let targetSelector = ann.target && ann.target.selector && ann.target.selector[0];
if (targetSelector) {
if (targetSelector.subBlockId) {
identifierText = `子块: ${targetSelector.subBlockId}`;
} else if (targetSelector.blockIndex !== undefined) {
identifierText = `块: ${targetSelector.blockIndex}`;
}
}
row.insertCell().textContent = identifierText;
// 增强文本片段预览(优先原始 Markdown,再回退 exact)
const textCell = row.insertCell();
let textSnippet = '[无文本片段]';
if (targetSelector) {
// 优先:以所属块的原始 Markdown 作为预览
let preferredBlockIndex = null;
if (targetSelector.blockIndex !== undefined && targetSelector.blockIndex !== null) {
preferredBlockIndex = parseInt(targetSelector.blockIndex, 10);
} else if (targetSelector.subBlockId) {
const parts = String(targetSelector.subBlockId).split('.');
if (parts.length > 0 && !isNaN(parseInt(parts[0], 10))) preferredBlockIndex = parseInt(parts[0], 10);
}
if (preferredBlockIndex !== null &&
window.currentBlockTokensForCopy &&
window.currentBlockTokensForCopy[ann.targetType] &&
window.currentBlockTokensForCopy[ann.targetType][preferredBlockIndex] &&
typeof window.currentBlockTokensForCopy[ann.targetType][preferredBlockIndex].raw === 'string') {
textSnippet = window.currentBlockTokensForCopy[ann.targetType][preferredBlockIndex].raw;
} else if (targetSelector.exact) {
// 回退:展示 exact(渲染后的文本片段)
textSnippet = targetSelector.exact;
}
}
// 限制显示长度,但保留更多内容
if (textSnippet && textSnippet.length > 150) {
textCell.textContent = textSnippet.substring(0, 150) + '...';
textCell.title = textSnippet; // 鼠标悬停时显示完整内容
} else {
textCell.textContent = textSnippet || '[无文本片段]';
}
// 添加查看完整内容的功能
if (textSnippet && textSnippet.length > 50) {
textCell.style.cursor = 'pointer';
textCell.addEventListener('click', function() {
const fullTextDialog = document.createElement('div');
fullTextDialog.className = 'full-text-dialog';
fullTextDialog.style.position = 'fixed';
fullTextDialog.style.top = '50%';
fullTextDialog.style.left = '50%';
fullTextDialog.style.transform = 'translate(-50%, -50%)';
fullTextDialog.style.maxWidth = '80%';
fullTextDialog.style.maxHeight = '80%';
fullTextDialog.style.backgroundColor = 'white';
fullTextDialog.style.padding = '20px';
fullTextDialog.style.borderRadius = '8px';
fullTextDialog.style.boxShadow = '0 0 20px rgba(0,0,0,0.3)';
fullTextDialog.style.zIndex = '10000';
fullTextDialog.style.overflow = 'auto';
const closeBtn = document.createElement('button');
closeBtn.textContent = '关闭';
closeBtn.style.position = 'absolute';
closeBtn.style.top = '10px';
closeBtn.style.right = '10px';
closeBtn.style.padding = '5px 10px';
closeBtn.style.cursor = 'pointer';
const content = document.createElement('pre');
content.style.whiteSpace = 'pre-wrap';
content.style.wordBreak = 'break-word';
content.style.margin = '10px 0';
content.style.padding = '10px';
content.style.backgroundColor = '#f5f5f5';
content.style.border = '1px solid #ddd';
content.style.borderRadius = '4px';
content.textContent = textSnippet;
fullTextDialog.appendChild(closeBtn);
fullTextDialog.appendChild(content);
document.body.appendChild(fullTextDialog);
closeBtn.onclick = function() {
if (fullTextDialog.parentNode === document.body) {
document.body.removeChild(fullTextDialog);
}
};
// 点击对话框外部关闭
document.addEventListener('click', function closeDialog(e) {
if (!fullTextDialog.contains(e.target) && e.target !== textCell) {
// 添加检查确保对话框仍然存在于文档中
if (fullTextDialog.parentNode === document.body) {
document.body.removeChild(fullTextDialog);
}
document.removeEventListener('click', closeDialog);
}
});
});
}
// ======= 可编辑笔记列 =======
const noteCell = row.insertCell();
const noteText = ann.body && ann.body.length > 0 && ann.body[0].value ? ann.body[0].value : '';
noteCell.textContent = noteText;
// 带批注类型 或 仅高亮类型都可编辑/添加
if (ann.motivation === 'commenting' || ann.motivation === 'highlighting') {
noteCell.style.cursor = 'pointer';
noteCell.title = noteText ? '点击编辑批注' : '点击添加批注';
if (!noteText && ann.motivation === 'highlighting') {
noteCell.style.color = '#aaa';
noteCell.textContent = '点击添加批注';
}
noteCell.addEventListener('click', function onEditNoteCell(e) {
if (noteCell.querySelector('textarea')) return; // 已经在编辑
const textarea = document.createElement('textarea');
textarea.value = noteText;
textarea.style.width = '98%';
textarea.style.minHeight = '40px';
textarea.style.fontSize = '13px';
textarea.style.fontFamily = 'inherit';
textarea.style.resize = 'vertical';
textarea.autofocus = true;
noteCell.innerHTML = '';
noteCell.appendChild(textarea);
textarea.focus();
// 保存函数
const saveNote = async () => {
const newVal = textarea.value.trim();
if (newVal === noteText || (!newVal && !noteText)) {
noteCell.textContent = noteText || '点击添加批注';
if (!noteText && ann.motivation === 'highlighting') noteCell.style.color = '#aaa';
return;
}
if (!newVal) {
noteCell.textContent = '内容不能为空';
setTimeout(() => { noteCell.textContent = noteText || '点击添加批注'; if (!noteText && ann.motivation === 'highlighting') noteCell.style.color = '#aaa'; }, 1200);
return;
}
noteCell.textContent = '保存中...';
try {
ann.body = [{ type: 'TextualBody', value: newVal, format: 'text/plain', purpose: 'commenting' }];
ann.modified = new Date().toISOString();
ann.motivation = 'commenting'; // 升级为批注
// 检查 updateAnnotationInDB 函数是否可用
if (typeof updateAnnotationInDB !== 'function') {
console.error('updateAnnotationInDB 函数未定义,请确保 storage.js 已正确加载');
throw new Error('保存批注的函数未定义');
}
await updateAnnotationInDB(ann);
// 更新window.data.annotations
const idx = window.data.annotations.findIndex(a => a.id === ann.id);
if (idx > -1) window.data.annotations[idx] = { ...ann };
// 刷新表格,保持筛选
const checkedColors = Array.from(annotationsSummaryColorFilter.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value);
populateAnnotationsSummaryTable(annotationsFilterTypeSelect.value, annotationsFilterContentSelect.value, checkedColors);
} catch (err) {
console.error('保存批注失败:', err);
noteCell.textContent = '[保存失败]';
setTimeout(() => { noteCell.textContent = noteText || '点击添加批注'; if (!noteText && ann.motivation === 'highlighting') noteCell.style.color = '#aaa'; }, 1500);
}
};
textarea.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
saveNote();
} else if (e.key === 'Escape') {
noteCell.textContent = noteText || '点击添加批注';
if (!noteText && ann.motivation === 'highlighting') noteCell.style.color = '#aaa';
}
});
textarea.addEventListener('blur', saveNote);
});
}
// ======= 可编辑颜色列 =======
const colorCell = row.insertCell();
if (ann.highlightColor) {
const swatch = document.createElement('span');
swatch.className = 'color-swatch';
swatch.style.backgroundColor = typeof getHighlightColor === 'function' ? getHighlightColor(ann.highlightColor) : ann.highlightColor;
swatch.style.cursor = 'pointer';
swatch.title = '点击更改高亮颜色';
colorCell.appendChild(swatch);
// 点击弹出下拉
swatch.addEventListener('click', function onEditColor(e) {
if (colorCell.querySelector('select')) return;
// 颜色选项:常用色+所有已用色
const commonColors = ['yellow','pink','lightblue','lightgreen','purple','orange','red','cyan','blue','green'];
const allColors = Array.from(new Set([...commonColors, ...getAllHighlightColors()]));
const select = document.createElement('select');
select.style.marginLeft = '6px';
allColors.forEach(c => {
const opt = document.createElement('option');
opt.value = c;
opt.textContent = c;
opt.style.backgroundColor = typeof getHighlightColor === 'function' ? getHighlightColor(c) : c;
if (c === ann.highlightColor) opt.selected = true;
select.appendChild(opt);
});
colorCell.appendChild(select);
select.focus();
// 保存颜色
const saveColor = async () => {
const newColor = select.value;
if (newColor === ann.highlightColor) {
colorCell.innerHTML = '';
colorCell.appendChild(swatch);
return;
}
colorCell.textContent = '保存中...';
try {
ann.highlightColor = newColor;
ann.modified = new Date().toISOString();
if (ann.motivation !== 'commenting') ann.motivation = 'highlighting';
// 检查 updateAnnotationInDB 函数是否可用
if (typeof updateAnnotationInDB !== 'function') {
console.error('updateAnnotationInDB 函数未定义,请确保 storage.js 已正确加载');
throw new Error('保存批注颜色的函数未定义');
}
await updateAnnotationInDB(ann);
// 更新window.data.annotations
const idx = window.data.annotations.findIndex(a => a.id === ann.id);
if (idx > -1) window.data.annotations[idx] = { ...ann };
// 刷新表格,保持筛选
const checkedColors = Array.from(annotationsSummaryColorFilter.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value);
populateAnnotationsSummaryTable(annotationsFilterTypeSelect.value, annotationsFilterContentSelect.value, checkedColors);
} catch (err) {
console.error('保存批注颜色失败:', err);
colorCell.textContent = '[保存失败]';
setTimeout(() => { colorCell.innerHTML = ''; colorCell.appendChild(swatch); }, 1500);
}
};
select.addEventListener('change', saveColor);
select.addEventListener('blur', saveColor);
});
} else {
colorCell.textContent = '-';
}
const actionsCell = row.insertCell();
const jumpButton = document.createElement('button');
jumpButton.textContent = '跳转';
jumpButton.className = 'action-btn';
jumpButton.dataset.annotationId = ann.id;
jumpButton.dataset.targetType = ann.targetType;
// 为跨子块批注提供回退子块ID与块索引(未分组场景)
let dataBlockIndex2 = (targetSelector && targetSelector.blockIndex !== undefined) ? String(targetSelector.blockIndex) : '';
let dataSubBlockId2 = (targetSelector && targetSelector.subBlockId !== undefined) ? String(targetSelector.subBlockId) : '';
if (!dataSubBlockId2 && targetSelector && targetSelector.type === 'CrossBlockRangeSelector') {
if (Array.isArray(targetSelector.affectedSubBlocks) && targetSelector.affectedSubBlocks.length > 0) {
dataSubBlockId2 = String(targetSelector.affectedSubBlocks[0]);
} else if (targetSelector.startSubBlockId) {
dataSubBlockId2 = String(targetSelector.startSubBlockId);
}
}
if (!dataBlockIndex2 && dataSubBlockId2) {
const parts = dataSubBlockId2.split('.');
if (parts.length > 0 && parts[0]) dataBlockIndex2 = parts[0];
}
jumpButton.dataset.blockIndex = dataBlockIndex2;
jumpButton.dataset.subBlockId = dataSubBlockId2;
// 仅在分块对比模式禁用跳转
if (window.currentVisibleTabId === 'chunk-compare') {
jumpButton.disabled = true;
jumpButton.title = '分块对比模式下禁用跳转';
}
jumpButton.onclick = async function() {
closeAnnotationsSummaryModal(); // Close modal before jumping
const targetType = this.dataset.targetType;
const blockIndex = this.dataset.blockIndex;
const subBlockId = this.dataset.subBlockId;
if (!targetType) return;
// Switch tab if necessary
if (window.currentVisibleTabId !== targetType && window.currentVisibleTabId !== `${targetType}-content-wrapper`) { // Check against common tab ID patterns
if (typeof showTab === 'function') {
console.log(`Switching to tab: ${targetType} for jump action.`);
await Promise.resolve(showTab(targetType)); // Ensure tab switch completes
// Wait a brief moment for DOM updates after tab switch, if showTab involves async rendering.
await new Promise(resolve => setTimeout(resolve, 200));
} else {
alert('无法切换标签页。');
return;
}
}
// 优先:等待目标高亮/元素出现后再跳转(处理分批渲染/分块延时)
if (typeof window.scrollToAnnotationAsync === 'function') {
const okAsync = await window.scrollToAnnotationAsync(this.dataset.annotationId, {
targetType,
subBlockId,
blockIndex,
timeoutMs: 5000,
pollIntervalMs: 120
});
if (okAsync) return;
} else if (typeof window.scrollToAnnotation === 'function') {
const ok = window.scrollToAnnotation(this.dataset.annotationId, true);
if (ok) return;
}
// 回退:按 subBlockId 或 blockIndex 定位
let elementToJump = null;
const contentWrapperId = `${targetType}-content-wrapper`;
const contentWrapper = document.getElementById(contentWrapperId);
if (contentWrapper) {
if (subBlockId && subBlockId !== 'undefined' && subBlockId !== '') {
elementToJump = contentWrapper.querySelector(`.sub-block[data-sub-block-id="${subBlockId}"]`);
} else if (blockIndex !== '') {
elementToJump = contentWrapper.querySelector(`[data-block-index="${blockIndex}"]`);
}
}
if (elementToJump) {
elementToJump.scrollIntoView({ behavior: 'smooth', block: 'center' });
elementToJump.classList.add('jump-to-highlight-effect');
setTimeout(() => elementToJump.classList.remove('jump-to-highlight-effect'), 2500);
} else {
alert('在当前视图中未找到目标元素。它可能已被过滤或不存在。');
console.warn('Element not found for jump:', {targetType, blockIndex, subBlockId});
}
};
actionsCell.appendChild(jumpButton);
});
}
if (!groupCount && !ungrouped.length) {
annotationsSummaryTableBody.innerHTML = '| 没有符合筛选条件的批注或高亮。 |
';
return;
}
}
window.openAnnotationsSummaryModal = function(filterByType = 'all', filterByContent = 'all') {
if (!annotationsSummaryModal) return;
// Set initial filter values from parameters
annotationsFilterTypeSelect.value = filterByType;
annotationsFilterContentSelect.value = filterByContent;
// 默认全选所有颜色
const allColors = getAllHighlightColors();
populateAnnotationsSummaryTable(filterByType, filterByContent, allColors);
annotationsSummaryModal.classList.add('visible');
};
function closeAnnotationsSummaryModal() {
if (annotationsSummaryModal) {
annotationsSummaryModal.classList.remove('visible');
}
}
if (annotationsSummaryCloseBtn) {
annotationsSummaryCloseBtn.onclick = closeAnnotationsSummaryModal;
}
if (annotationsSummaryModal) {
annotationsSummaryModal.addEventListener('click', function(event) {
if (event.target === annotationsSummaryModal) { // Click on overlay
closeAnnotationsSummaryModal();
}
});
}
if (annotationsFilterTypeSelect && annotationsFilterContentSelect) {
annotationsFilterTypeSelect.addEventListener('change', () => {
// 颜色多选保持当前选中
const checkedColors = Array.from(annotationsSummaryColorFilter.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value);
populateAnnotationsSummaryTable(annotationsFilterTypeSelect.value, annotationsFilterContentSelect.value, checkedColors);
});
annotationsFilterContentSelect.addEventListener('change', () => {
const checkedColors = Array.from(annotationsSummaryColorFilter.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value);
populateAnnotationsSummaryTable(annotationsFilterTypeSelect.value, annotationsFilterContentSelect.value, checkedColors);
});
}
// 存储上次看到的颜色集合,用于检测新颜色
let lastSeenColors = new Set();
// 定期检查是否有新颜色出现
function checkForNewColors() {
const currentColors = new Set(getAllHighlightColors());
// 找出新增的颜色
const newColors = Array.from(currentColors).filter(color => !lastSeenColors.has(color));
// 如果有新颜色且模态框当前可见
if (newColors.length > 0 && annotationsSummaryModal && annotationsSummaryModal.classList.contains('visible')) {
console.log('检测到新颜色:', newColors);
// 获取当前已选中的颜色
const currentlyCheckedColors = Array.from(
annotationsSummaryColorFilter.querySelectorAll('input[type="checkbox"]:checked')
).map(cb => cb.value);
// 合并当前选中的颜色和新颜色
const updatedSelection = [...currentlyCheckedColors, ...newColors];
// 重新渲染表格,确保新颜色被选中
populateAnnotationsSummaryTable(
annotationsFilterTypeSelect.value,
annotationsFilterContentSelect.value,
updatedSelection
);
}
// 更新已知颜色集合
lastSeenColors = currentColors;
}
// 初始化已知颜色集合
lastSeenColors = new Set(getAllHighlightColors());
// 每秒检查一次新颜色
// 性能优化:页面隐藏时暂停轮询
(function() {
let timerId = null;
let isActive = false;
function poll() {
if (!isActive) return;
if (!document.hidden) {
checkForNewColors();
}
timerId = setTimeout(poll, 1000);
}
function start() {
if (isActive) return;
isActive = true;
poll();
}
function stop() {
isActive = false;
if (timerId) {
clearTimeout(timerId);
timerId = null;
}
}
document.addEventListener('visibilitychange', () => {
// 页面隐藏/显示时无需额外操作,poll 函数会自动跳过
});
window.addEventListener('beforeunload', stop);
start();
})();