Commit 6be9711c by pangchong

feat(editor): 优化大纲视图和XML树视图滚动条交互体验

- 为大纲视图和XML树视图添加自定义虚拟列表滚动条,解决节点过多时滚动条拖拽困难问题
- 实现滚动条拖拽与点击轨道跳转功能,提升操作流畅度
- 使用ResizeObserver动态响应滚动区域尺寸变化,保证滚动条高度准确
- 隐藏原生滚动条,统一滚动条样式风格
- 优化大纲视图节点的样式和交互细节,增加图标和序号徽标的动态效果
- XML树视图节点支持展开折叠、批量管理模式下复选框显示
- 增加大纲视图和XML树视图滚动事件处理,确保滚动状态同步更新
parent 2b95d913
......@@ -59,4 +59,20 @@
/* 全局弹框组件样式 */
.app-modal {
border-radius: 12px;
}
/* OverlayScrollbars 主题重写 - 解决虚拟列表节点过多时滑块极小难拖拽的问题 */
.os-theme-custom {
--os-size: 10px;
--os-padding-perpendicular: 1px;
--os-padding-axis: 2px;
--os-track-bg: rgba(128, 128, 128, 0.05);
--os-track-bg-hover: rgba(128, 128, 128, 0.12);
--os-track-bg-active: rgba(128, 128, 128, 0.18);
--os-track-border-radius: 6px;
--os-handle-bg: rgba(128, 128, 128, 0.4);
--os-handle-bg-hover: var(--primary-color, #705bf6);
--os-handle-bg-active: var(--primary-color, #705bf6);
--os-handle-border-radius: 6px;
--os-handle-min-size: 35px;
}
\ No newline at end of file
......@@ -13,3 +13,5 @@ export interface DocOutlineProps {
pattern: string
active: boolean
}
export const MIN_THUMB_SIZE = 36
import { useEditorStore } from '@/store/editor'
import type { DocOutlineProps, OutlineItem } from '../constants'
import { MIN_THUMB_SIZE } from '../constants'
export function useDocOutlineView(props: DocOutlineProps) {
const editorStore = useEditorStore()
......@@ -362,13 +363,105 @@ export function useDocOutlineView(props: DocOutlineProps) {
}
)
const handleContainerScroll = (e: Event) => {
if (outlineViewportRef.value) {
scrollTop.value = outlineViewportRef.value.scrollTop
viewportHeight.value = outlineViewportRef.value.clientHeight
}
handleOutlineScroll(e)
}
let resizeObserver: ResizeObserver | null = null
onMounted(() => {
if (outlineViewportRef.value) {
viewportHeight.value = outlineViewportRef.value.clientHeight
resizeObserver = new ResizeObserver((entries) => {
if (entries[0]) {
viewportHeight.value = entries[0].contentRect.height
}
})
resizeObserver.observe(outlineViewportRef.value)
}
})
onUnmounted(() => {
resizeObserver?.disconnect()
resizeObserver = null
document.removeEventListener('mousemove', handleThumbDragging)
document.removeEventListener('mouseup', handleThumbDragEnd)
})
const showCustomScrollbar = computed(() => {
return totalHeight.value > viewportHeight.value && viewportHeight.value > 0
})
const thumbHeight = computed(() => {
if (!viewportHeight.value || !totalHeight.value) return MIN_THUMB_SIZE
const raw = (viewportHeight.value / totalHeight.value) * viewportHeight.value
return Math.max(MIN_THUMB_SIZE, Math.min(viewportHeight.value, Math.round(raw)))
})
const thumbTop = computed(() => {
const maxScroll = totalHeight.value - viewportHeight.value
const maxThumb = viewportHeight.value - thumbHeight.value
if (maxScroll <= 0 || maxThumb <= 0) return 0
return (scrollTop.value / maxScroll) * maxThumb
})
let isDragging = false
let startY = 0
let startScrollTop = 0
const handleThumbDragStart = (e: MouseEvent) => {
isDragging = true
startY = e.clientY
startScrollTop = outlineViewportRef.value?.scrollTop || 0
document.addEventListener('mousemove', handleThumbDragging)
document.addEventListener('mouseup', handleThumbDragEnd)
}
const handleThumbDragging = (e: MouseEvent) => {
if (!isDragging || !outlineViewportRef.value) return
const deltaY = e.clientY - startY
const maxScroll = totalHeight.value - viewportHeight.value
const maxThumb = viewportHeight.value - thumbHeight.value
if (maxThumb <= 0) return
const scrollDelta = (deltaY / maxThumb) * maxScroll
outlineViewportRef.value.scrollTop = startScrollTop + scrollDelta
}
const handleThumbDragEnd = () => {
isDragging = false
document.removeEventListener('mousemove', handleThumbDragging)
document.removeEventListener('mouseup', handleThumbDragEnd)
}
const handleTrackClick = (e: MouseEvent) => {
if (!outlineViewportRef.value) return
const trackRect = (e.currentTarget as HTMLElement).getBoundingClientRect()
const clickY = e.clientY - trackRect.top
const maxScroll = totalHeight.value - viewportHeight.value
const maxThumb = viewportHeight.value - thumbHeight.value
if (maxThumb <= 0) return
const targetThumbTop = clickY - thumbHeight.value / 2
const clampedThumbTop = Math.max(0, Math.min(maxThumb, targetThumbTop))
const targetScrollTop = (clampedThumbTop / maxThumb) * maxScroll
outlineViewportRef.value.scrollTop = targetScrollTop
}
return {
editorStore,
outlineViewportRef,
filteredOutlineList,
activeOutlineId,
showCustomScrollbar,
thumbHeight,
thumbTop,
handleContainerScroll,
handleThumbDragStart,
handleTrackClick,
handleOutlineClick,
handleOutlineScroll,
totalHeight,
startOffset,
visibleOutlineItems
......
......@@ -21,3 +21,5 @@ export interface XmlTreeViewProps {
hasCustomColor: (item: FlatNode) => boolean
getNodeStyle: (item: FlatNode) => any
}
export const MIN_THUMB_SIZE = 36
import { useEditorStore } from '@/store/editor'
import type { XmlTreeViewProps } from '../constants'
import { MIN_THUMB_SIZE } from '../constants'
export function useXmlTreeView(_props: XmlTreeViewProps) {
export function useXmlTreeView(props: XmlTreeViewProps, emit: any) {
const editorStore = useEditorStore()
const viewportRef = ref<HTMLElement | null>(null)
const scrollTop = ref(0)
const viewportHeight = ref(0)
const handleContainerScroll = (e: Event) => {
if (viewportRef.value) {
scrollTop.value = viewportRef.value.scrollTop
viewportHeight.value = viewportRef.value.clientHeight
}
emit('scroll', e)
}
let resizeObserver: ResizeObserver | null = null
onMounted(() => {
if (viewportRef.value) {
viewportHeight.value = viewportRef.value.clientHeight
resizeObserver = new ResizeObserver((entries) => {
if (entries[0]) {
viewportHeight.value = entries[0].contentRect.height
}
})
resizeObserver.observe(viewportRef.value)
}
})
onUnmounted(() => {
resizeObserver?.disconnect()
resizeObserver = null
document.removeEventListener('mousemove', handleThumbDragging)
document.removeEventListener('mouseup', handleThumbDragEnd)
})
const showCustomScrollbar = computed(() => {
return props.totalHeight > viewportHeight.value && viewportHeight.value > 0
})
const thumbHeight = computed(() => {
if (!viewportHeight.value || !props.totalHeight) return MIN_THUMB_SIZE
const raw = (viewportHeight.value / props.totalHeight) * viewportHeight.value
return Math.max(MIN_THUMB_SIZE, Math.min(viewportHeight.value, Math.round(raw)))
})
const thumbTop = computed(() => {
const maxScroll = props.totalHeight - viewportHeight.value
const maxThumb = viewportHeight.value - thumbHeight.value
if (maxScroll <= 0 || maxThumb <= 0) return 0
return (scrollTop.value / maxScroll) * maxThumb
})
let isDragging = false
let startY = 0
let startScrollTop = 0
const handleThumbDragStart = (e: MouseEvent) => {
isDragging = true
startY = e.clientY
startScrollTop = viewportRef.value?.scrollTop || 0
document.addEventListener('mousemove', handleThumbDragging)
document.addEventListener('mouseup', handleThumbDragEnd)
}
const handleThumbDragging = (e: MouseEvent) => {
if (!isDragging || !viewportRef.value) return
const deltaY = e.clientY - startY
const maxScroll = props.totalHeight - viewportHeight.value
const maxThumb = viewportHeight.value - thumbHeight.value
if (maxThumb <= 0) return
const scrollDelta = (deltaY / maxThumb) * maxScroll
viewportRef.value.scrollTop = startScrollTop + scrollDelta
}
const handleThumbDragEnd = () => {
isDragging = false
document.removeEventListener('mousemove', handleThumbDragging)
document.removeEventListener('mouseup', handleThumbDragEnd)
}
const handleTrackClick = (e: MouseEvent) => {
if (!viewportRef.value) return
const trackRect = (e.currentTarget as HTMLElement).getBoundingClientRect()
const clickY = e.clientY - trackRect.top
const maxScroll = props.totalHeight - viewportHeight.value
const maxThumb = viewportHeight.value - thumbHeight.value
if (maxThumb <= 0) return
const targetThumbTop = clickY - thumbHeight.value / 2
const clampedThumbTop = Math.max(0, Math.min(maxThumb, targetThumbTop))
const targetScrollTop = (clampedThumbTop / maxThumb) * maxScroll
viewportRef.value.scrollTop = targetScrollTop
}
const getGraphicKey = (node: any): string => {
let key = node.attributes?.KEY || node.attributes?.GRAPHICKEY || node.attributes?.ID || ''
if (!key && node.children) {
......@@ -56,6 +147,12 @@ export function useXmlTreeView(_props: XmlTreeViewProps) {
return {
editorStore,
viewportRef,
showCustomScrollbar,
thumbHeight,
thumbTop,
handleContainerScroll,
handleThumbDragStart,
handleTrackClick,
getGraphicKey,
getTreeItemRefNodes,
getTreeItemTargetId,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment