diff --git a/web/src/components/DocRenderer.vue b/web/src/components/DocRenderer.vue index cb41bc2..ef5cfa4 100644 --- a/web/src/components/DocRenderer.vue +++ b/web/src/components/DocRenderer.vue @@ -126,6 +126,18 @@ function injectStyles(doc: Document) { outline-offset: 2px; background-color: rgba(154, 161, 173, 0.06) !important; } + [data-block-index].block-variable { + outline: 2px solid rgba(26, 140, 74, 0.5) !important; + outline-offset: 2px; + background-color: rgba(26, 140, 74, 0.06) !important; + } + .block-list-item.list-unordered::before { + content: "\\2022"; + display: inline-block; + width: 1em; + margin-left: -1em; + color: #333; + } ` doc.head.appendChild(style) } @@ -147,126 +159,137 @@ function bindIframeEvents(doc: Document) { el.addEventListener('mouseleave', () => { el.classList.remove('hover-highlight') }) - - // 点击选中 - el.addEventListener('click', (e: Event) => { - if (mode.value !== 'select') return - const mouseEvent = e as MouseEvent - const index = parseInt(el.getAttribute('data-block-index') || '') - if (isNaN(index)) return - - if (mouseEvent.ctrlKey || mouseEvent.metaKey) { - // Ctrl+点击:追加/取消选中 - if (selectedIndices.value.has(index)) { - selectedIndices.value.delete(index) - el.classList.remove('block-selected') - } else { - selectedIndices.value.add(index) - el.classList.add('block-selected') - } - selectedIndices.value = new Set(selectedIndices.value) - } else { - // 普通点击:单选 - clearAllSelection(doc) - selectedIndices.value = new Set([index]) - el.classList.add('block-selected') - } - emitBlockSelect() - }) }) - // 拖拽框选 + // 统一处理单击选中 与 拖拽框选:在 mousedown 记录起点,mouseup 时根据移动距离判断 + let mouseDownTarget: HTMLElement | null = null + let mouseMoved = false + doc.addEventListener('mousedown', (e: MouseEvent) => { if (mode.value !== 'select') return - // 只在文档区域(非 block 元素上)开始拖拽 - const target = e.target as HTMLElement - if (target.hasAttribute('data-block-index')) return - - isDragging.value = true - showSelectionRect.value = true + mouseDownTarget = e.target as HTMLElement + mouseMoved = false + isDragging.value = false + showSelectionRect.value = false dragStart.value = { x: e.clientX, y: e.clientY } selectionRect.value = { left: e.clientX, top: e.clientY, width: 0, height: 0 } + + // 非 Ctrl 按下时,提前清除旧选中(给用户即时的视觉反馈) + if (!(e.ctrlKey || e.metaKey)) { + clearAllSelection(doc) + selectedIndices.value = new Set() + } }) doc.addEventListener('mousemove', (e: MouseEvent) => { - if (!isDragging.value || mode.value !== 'select') return - selectionRect.value = { - left: Math.min(dragStart.value.x, e.clientX), - top: Math.min(dragStart.value.y, e.clientY), - width: Math.abs(e.clientX - dragStart.value.x), - height: Math.abs(e.clientY - dragStart.value.y), + if (mode.value !== 'select') return + if (dragStart.value.x === 0 && dragStart.value.y === 0) return + const dx = Math.abs(e.clientX - dragStart.value.x) + const dy = Math.abs(e.clientY - dragStart.value.y) + if (dx > 3 || dy > 3) { + mouseMoved = true + // 开始显示框选矩形 + if (!isDragging.value) { + isDragging.value = true + showSelectionRect.value = true + } + selectionRect.value = { + left: Math.min(dragStart.value.x, e.clientX), + top: Math.min(dragStart.value.y, e.clientY), + width: Math.abs(e.clientX - dragStart.value.x), + height: Math.abs(e.clientY - dragStart.value.y), + } } }) doc.addEventListener('mouseup', (e: MouseEvent) => { - if (!isDragging.value) return - isDragging.value = false - showSelectionRect.value = false + if (mode.value !== 'select') return - // 计算拖拽距离,判断是框选还是取消 - const dx = Math.abs(e.clientX - dragStart.value.x) - const dy = Math.abs(e.clientY - dragStart.value.y) - if (dx < 5 && dy < 5) { - // 微小移动,忽略 - selectionRect.value = { left: 0, top: 0, width: 0, height: 0 } - return - } + if (mouseMoved && isDragging.value) { + // 拖拽框选:计算选区覆盖的 blocks + isDragging.value = false + showSelectionRect.value = false - // 计算选区覆盖的 blocks - const rect = iframeRef.value?.getBoundingClientRect() - if (!rect) return - clearAllSelection(doc) - const newSelected = new Set() - - blocks.forEach(el => { - const blockRect = el.getBoundingClientRect() - // 判断 block 是否与选择矩形相交 + const iframeRect = iframeRef.value?.getBoundingClientRect() + if (!iframeRect) { + dragStart.value = { x: 0, y: 0 } + return + } + const newSelected = new Set(selectedIndices.value) const selRect = { left: selectionRect.value.left, top: selectionRect.value.top, right: selectionRect.value.left + selectionRect.value.width, bottom: selectionRect.value.top + selectionRect.value.height, } - const bRect = { - left: blockRect.left, - top: blockRect.top, - right: blockRect.right, - bottom: blockRect.bottom, - } - if ( - selRect.left < bRect.right && - selRect.right > bRect.left && - selRect.top < bRect.bottom && - selRect.bottom > bRect.top - ) { - const index = parseInt(el.getAttribute('data-block-index') || '') - if (!isNaN(index)) { - newSelected.add(index) - el.classList.add('block-selected') + blocks.forEach(el => { + const br = el.getBoundingClientRect() + if ( + selRect.left < br.right && + selRect.right > br.left && + selRect.top < br.bottom && + selRect.bottom > br.top + ) { + const index = parseInt(el.getAttribute('data-block-index') || '') + if (!isNaN(index)) { + newSelected.add(index) + el.classList.add('block-selected') + } } + }) + selectedIndices.value = newSelected + selectionRect.value = { left: 0, top: 0, width: 0, height: 0 } + + if (newSelected.size > 0) { + showContextMenu.value = true + contextMenuPos.value = { x: e.clientX, y: e.clientY + 10 } + } else { + showContextMenu.value = false } - }) - - selectedIndices.value = newSelected - selectionRect.value = { left: 0, top: 0, width: 0, height: 0 } - - if (newSelected.size > 0) { - showContextMenu.value = true - contextMenuPos.value = { x: e.clientX, y: e.clientY + 10 } - } - emitBlockSelect() - }) - - // 点击空白处取消选中 - doc.addEventListener('click', (e: MouseEvent) => { - if (mode.value !== 'select') return - const target = e.target as HTMLElement - if (!target.hasAttribute('data-block-index') && !isDragging.value) { - clearAllSelection(doc) - selectedIndices.value = new Set() - showContextMenu.value = false emitBlockSelect() + } else { + // 短点击(没有拖动):单击选中或取消 + isDragging.value = false + showSelectionRect.value = false + selectionRect.value = { left: 0, top: 0, width: 0, height: 0 } + + const target = e.target as HTMLElement + const blockEl = target.closest?.('[data-block-index]') as HTMLElement | null + if (blockEl) { + const index = parseInt(blockEl.getAttribute('data-block-index') || '') + if (!isNaN(index)) { + if (e.ctrlKey || e.metaKey) { + // Ctrl+点击:追加/取消 + if (selectedIndices.value.has(index)) { + selectedIndices.value.delete(index) + blockEl.classList.remove('block-selected') + } else { + selectedIndices.value.add(index) + blockEl.classList.add('block-selected') + } + selectedIndices.value = new Set(selectedIndices.value) + } else { + // 普通点击:单选该块(已在 mousedown 清空,直接添加) + selectedIndices.value = new Set([index]) + blockEl.classList.add('block-selected') + } + emitBlockSelect() + if (selectedIndices.value.size > 0) { + showContextMenu.value = true + contextMenuPos.value = { x: e.clientX, y: e.clientY + 10 } + } + } + } else { + // 点击空白区域:取消所有选中 + clearAllSelection(doc) + selectedIndices.value = new Set() + showContextMenu.value = false + emitBlockSelect() + } } + + dragStart.value = { x: 0, y: 0 } + mouseMoved = false }) }