A:调整文件夹
This commit is contained in:
1 parent
7af4f31538
commit
bf5e44dca3
6 files changed
+372
-163
No files matched your search
@@ -54,10 +54,17 @@
|
|||||||
<button
|
<button
|
||||||
:class="['btn-layout', { active: layoutMode === 'horizontal' }]"
|
:class="['btn-layout', { active: layoutMode === 'horizontal' }]"
|
||||||
@click="layoutMode = 'horizontal'"
|
@click="layoutMode = 'horizontal'"
|
||||||
title="横向双面板"
|
title="左右双面板"
|
||||||
>
|
>
|
||||||
▭
|
▭
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
:class="['btn-layout', { active: layoutMode === 'vertical' }]"
|
||||||
|
@click="layoutMode = 'vertical'"
|
||||||
|
title="上下双面板"
|
||||||
|
>
|
||||||
|
▯
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,13 +80,13 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="layoutMode === 'horizontal'"
|
v-if="layoutMode === 'horizontal' || layoutMode === 'vertical'"
|
||||||
class="splitter"
|
:class="['splitter', layoutMode === 'vertical' ? 'splitter-vertical' : '']"
|
||||||
@mousedown="handleSplitterMouseDown"
|
@mousedown="handleSplitterMouseDown"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<FilePane
|
<FilePane
|
||||||
v-if="layoutMode === 'horizontal'"
|
v-if="layoutMode === 'horizontal' || layoutMode === 'vertical'"
|
||||||
pane-id="right"
|
pane-id="right"
|
||||||
:view-mode="viewMode"
|
:view-mode="viewMode"
|
||||||
:thumbnail-size="thumbnailSize"
|
:thumbnail-size="thumbnailSize"
|
||||||
@@ -105,7 +112,8 @@ const isDraggingSplitter = ref(false)
|
|||||||
|
|
||||||
function handleSplitterMouseDown(event: MouseEvent) {
|
function handleSplitterMouseDown(event: MouseEvent) {
|
||||||
isDraggingSplitter.value = true
|
isDraggingSplitter.value = true
|
||||||
document.body.style.cursor = 'col-resize'
|
const isVertical = layoutMode.value === 'vertical'
|
||||||
|
document.body.style.cursor = isVertical ? 'row-resize' : 'col-resize'
|
||||||
document.body.style.userSelect = 'none'
|
document.body.style.userSelect = 'none'
|
||||||
|
|
||||||
const mainContent = document.querySelector('.main-content') as HTMLElement
|
const mainContent = document.querySelector('.main-content') as HTMLElement
|
||||||
@@ -115,11 +123,21 @@ function handleSplitterMouseDown(event: MouseEvent) {
|
|||||||
if (!isDraggingSplitter.value) return
|
if (!isDraggingSplitter.value) return
|
||||||
|
|
||||||
const rect = mainContent.getBoundingClientRect()
|
const rect = mainContent.getBoundingClientRect()
|
||||||
const x = e.clientX - rect.left
|
|
||||||
const percentage = (x / rect.width) * 100
|
|
||||||
|
|
||||||
if (percentage > 20 && percentage < 80) {
|
if (isVertical) {
|
||||||
mainContent.style.gridTemplateColumns = `${percentage}% 4px 1fr`
|
const y = e.clientY - rect.top
|
||||||
|
const percentage = (y / rect.height) * 100
|
||||||
|
|
||||||
|
if (percentage > 20 && percentage < 80) {
|
||||||
|
mainContent.style.gridTemplateRows = `${percentage}% 4px 1fr`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const x = e.clientX - rect.left
|
||||||
|
const percentage = (x / rect.width) * 100
|
||||||
|
|
||||||
|
if (percentage > 20 && percentage < 80) {
|
||||||
|
mainContent.style.gridTemplateColumns = `${percentage}% 4px 1fr`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +158,7 @@ function handleSplitterMouseDown(event: MouseEvent) {
|
|||||||
.file-manager {
|
.file-manager {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
background: #f9fafb;
|
background: #f9fafb;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,11 +253,20 @@ function handleSplitterMouseDown(event: MouseEvent) {
|
|||||||
grid-template-columns: 1fr 4px 1fr;
|
grid-template-columns: 1fr 4px 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-content.layout-vertical {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr 4px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.splitter {
|
.splitter {
|
||||||
background: #d1d5db;
|
background: #d1d5db;
|
||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.splitter.splitter-vertical {
|
||||||
|
cursor: row-resize;
|
||||||
|
}
|
||||||
|
|
||||||
.splitter:hover {
|
.splitter:hover {
|
||||||
background: #9ca3af;
|
background: #9ca3af;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
@drop="handlePaneDrop"
|
@drop="handlePaneDrop"
|
||||||
>
|
>
|
||||||
<!-- Grid View -->
|
<!-- Grid View -->
|
||||||
<div v-if="viewMode === 'grid'" :class="['file-grid', `grid-${thumbnailSize}`]">
|
<div v-if="viewMode === 'grid'" :class="['file-grid', `grid-${thumbnailSize}`]" :style="gridItemSize">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in visibleItems"
|
v-for="(item, index) in visibleItems"
|
||||||
:key="item.name"
|
:key="item.name"
|
||||||
@@ -211,6 +211,7 @@ const selection = useFileSelection()
|
|||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const pathEditMode = ref(false)
|
const pathEditMode = ref(false)
|
||||||
const pathEditValue = ref('')
|
const pathEditValue = ref('')
|
||||||
|
const zoomLevel = ref(100) // 缩放级别:50-200%
|
||||||
|
|
||||||
// Context Menu state
|
// Context Menu state
|
||||||
const contextMenu = ref({
|
const contextMenu = ref({
|
||||||
@@ -237,7 +238,8 @@ const dragSelection = ref({
|
|||||||
currentX: 0,
|
currentX: 0,
|
||||||
currentY: 0,
|
currentY: 0,
|
||||||
initialSelections: new Set<string>(),
|
initialSelections: new Set<string>(),
|
||||||
ctrlKey: false
|
ctrlKey: false,
|
||||||
|
justFinished: false
|
||||||
})
|
})
|
||||||
|
|
||||||
// Auto scroll state
|
// Auto scroll state
|
||||||
@@ -486,13 +488,28 @@ function handleMouseDown(event: MouseEvent) {
|
|||||||
currentX: event.clientX - rect.left + scrollEl.scrollLeft,
|
currentX: event.clientX - rect.left + scrollEl.scrollLeft,
|
||||||
currentY: event.clientY - rect.top + scrollEl.scrollTop,
|
currentY: event.clientY - rect.top + scrollEl.scrollTop,
|
||||||
initialSelections: new Set(selection.selectedItems.value),
|
initialSelections: new Set(selection.selectedItems.value),
|
||||||
ctrlKey: event.ctrlKey || event.metaKey
|
ctrlKey: event.ctrlKey || event.metaKey,
|
||||||
|
justFinished: false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dragSelection.value.ctrlKey) {
|
if (!dragSelection.value.ctrlKey) {
|
||||||
selection.clearSelection()
|
selection.clearSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 在 document 级别监听 mousemove 和 mouseup
|
||||||
|
const onDocumentMouseMove = (e: MouseEvent) => {
|
||||||
|
handleMouseMove(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDocumentMouseUp = (e: MouseEvent) => {
|
||||||
|
handleMouseUp()
|
||||||
|
document.removeEventListener('mousemove', onDocumentMouseMove)
|
||||||
|
document.removeEventListener('mouseup', onDocumentMouseUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onDocumentMouseMove)
|
||||||
|
document.addEventListener('mouseup', onDocumentMouseUp)
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,8 +520,27 @@ function handleMouseMove(event: MouseEvent) {
|
|||||||
if (!rect) return
|
if (!rect) return
|
||||||
|
|
||||||
const scrollEl = contentAreaRef.value!
|
const scrollEl = contentAreaRef.value!
|
||||||
dragSelection.value.currentX = event.clientX - rect.left + scrollEl.scrollLeft
|
|
||||||
dragSelection.value.currentY = event.clientY - rect.top + scrollEl.scrollTop
|
// 计算鼠标相对于内容区域的位置(包含滚动偏移)
|
||||||
|
// 鼠标在视口中的位置 + 滚动偏移 = 在文档中的绝对位置
|
||||||
|
const rawX = event.clientX - rect.left + scrollEl.scrollLeft
|
||||||
|
const rawY = event.clientY - rect.top + scrollEl.scrollTop
|
||||||
|
|
||||||
|
// 限制坐标在合理范围内
|
||||||
|
// 当鼠标超出边界时,坐标应该停留在边界上,而不是继续增长
|
||||||
|
const maxScrollLeft = Math.max(0, scrollEl.scrollWidth - scrollEl.clientWidth)
|
||||||
|
const maxScrollTop = Math.max(0, scrollEl.scrollHeight - scrollEl.clientHeight)
|
||||||
|
|
||||||
|
// 最大坐标 = 可见区域大小 + 最大滚动距离
|
||||||
|
const maxX = scrollEl.clientWidth + maxScrollLeft
|
||||||
|
const maxY = scrollEl.clientHeight + maxScrollTop
|
||||||
|
|
||||||
|
const relativeX = Math.max(0, Math.min(rawX, maxX))
|
||||||
|
const relativeY = Math.max(0, Math.min(rawY, maxY))
|
||||||
|
|
||||||
|
// 存储坐标
|
||||||
|
dragSelection.value.currentX = relativeX
|
||||||
|
dragSelection.value.currentY = relativeY
|
||||||
|
|
||||||
// 自动滚动检测
|
// 自动滚动检测
|
||||||
checkAutoScroll(event)
|
checkAutoScroll(event)
|
||||||
@@ -521,6 +557,12 @@ function handleMouseUp() {
|
|||||||
dragSelection.value.isSelecting = false
|
dragSelection.value.isSelecting = false
|
||||||
autoScroll.value.isScrolling = false
|
autoScroll.value.isScrolling = false
|
||||||
|
|
||||||
|
// 标记刚完成拖拽,防止 handleContentClick 清除选择
|
||||||
|
dragSelection.value.justFinished = true
|
||||||
|
setTimeout(() => {
|
||||||
|
dragSelection.value.justFinished = false
|
||||||
|
}, 100)
|
||||||
|
|
||||||
// 设置焦点到最后选中的项
|
// 设置焦点到最后选中的项
|
||||||
if (selection.selectedItems.value.size > 0) {
|
if (selection.selectedItems.value.size > 0) {
|
||||||
const lastSelected = Array.from(selection.selectedItems.value).pop()!
|
const lastSelected = Array.from(selection.selectedItems.value).pop()!
|
||||||
@@ -537,21 +579,45 @@ function checkAutoScroll(event: MouseEvent) {
|
|||||||
if (!contentAreaRef.value) return
|
if (!contentAreaRef.value) return
|
||||||
|
|
||||||
const rect = contentAreaRef.value.getBoundingClientRect()
|
const rect = contentAreaRef.value.getBoundingClientRect()
|
||||||
|
const scrollEl = contentAreaRef.value
|
||||||
const threshold = 50
|
const threshold = 50
|
||||||
|
const baseScrollSpeed = 10
|
||||||
|
|
||||||
let dx = 0
|
let dx = 0
|
||||||
let dy = 0
|
let dy = 0
|
||||||
|
|
||||||
if (event.clientY < rect.top + threshold) {
|
// 检查垂直滚动
|
||||||
dy = -5
|
if (event.clientY < rect.top) {
|
||||||
|
dy = -baseScrollSpeed
|
||||||
|
} else if (event.clientY < rect.top + threshold) {
|
||||||
|
const distance = event.clientY - rect.top
|
||||||
|
const ratio = 1 - distance / threshold
|
||||||
|
dy = -Math.max(3, baseScrollSpeed * ratio)
|
||||||
|
} else if (event.clientY > rect.bottom) {
|
||||||
|
dy = baseScrollSpeed
|
||||||
} else if (event.clientY > rect.bottom - threshold) {
|
} else if (event.clientY > rect.bottom - threshold) {
|
||||||
dy = 5
|
const distance = rect.bottom - event.clientY
|
||||||
|
const ratio = 1 - distance / threshold
|
||||||
|
dy = Math.max(3, baseScrollSpeed * ratio)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.clientX < rect.left + threshold) {
|
// 检查水平滚动 - 只在确实有横向滚动空间时才启用
|
||||||
dx = -5
|
const hasHorizontalScroll = scrollEl.scrollWidth > scrollEl.clientWidth
|
||||||
} else if (event.clientX > rect.right - threshold) {
|
|
||||||
dx = 5
|
if (hasHorizontalScroll) {
|
||||||
|
if (event.clientX < rect.left) {
|
||||||
|
dx = -baseScrollSpeed
|
||||||
|
} else if (event.clientX < rect.left + threshold) {
|
||||||
|
const distance = event.clientX - rect.left
|
||||||
|
const ratio = 1 - distance / threshold
|
||||||
|
dx = -Math.max(3, baseScrollSpeed * ratio)
|
||||||
|
} else if (event.clientX > rect.right) {
|
||||||
|
dx = baseScrollSpeed
|
||||||
|
} else if (event.clientX > rect.right - threshold) {
|
||||||
|
const distance = rect.right - event.clientX
|
||||||
|
const ratio = 1 - distance / threshold
|
||||||
|
dx = Math.max(3, baseScrollSpeed * ratio)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dx !== 0 || dy !== 0) {
|
if (dx !== 0 || dy !== 0) {
|
||||||
@@ -569,23 +635,56 @@ function checkAutoScroll(event: MouseEvent) {
|
|||||||
|
|
||||||
// 开始自动滚动
|
// 开始自动滚动
|
||||||
function startAutoScroll() {
|
function startAutoScroll() {
|
||||||
|
let frameId: number | null = null
|
||||||
|
|
||||||
function scroll() {
|
function scroll() {
|
||||||
if (!autoScroll.value.isScrolling || !contentAreaRef.value) return
|
if (!autoScroll.value.isScrolling || !contentAreaRef.value) {
|
||||||
|
if (frameId !== null) {
|
||||||
|
cancelAnimationFrame(frameId)
|
||||||
|
frameId = null
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
contentAreaRef.value.scrollLeft += autoScroll.value.direction.x
|
const scrollEl = contentAreaRef.value
|
||||||
contentAreaRef.value.scrollTop += autoScroll.value.direction.y
|
|
||||||
|
|
||||||
// 更新拖拽坐标
|
// 计算新的滚动位置,并限制在有效范围内
|
||||||
const rect = contentAreaRef.value.getBoundingClientRect()
|
const maxScrollLeft = Math.max(0, scrollEl.scrollWidth - scrollEl.clientWidth)
|
||||||
dragSelection.value.currentX += autoScroll.value.direction.x
|
const maxScrollTop = Math.max(0, scrollEl.scrollHeight - scrollEl.clientHeight)
|
||||||
dragSelection.value.currentY += autoScroll.value.direction.y
|
|
||||||
|
|
||||||
updateDragSelection()
|
let newScrollLeft = scrollEl.scrollLeft + autoScroll.value.direction.x
|
||||||
|
let newScrollTop = scrollEl.scrollTop + autoScroll.value.direction.y
|
||||||
|
|
||||||
requestAnimationFrame(scroll)
|
// 限制滚动范围
|
||||||
|
newScrollLeft = Math.max(0, Math.min(newScrollLeft, maxScrollLeft))
|
||||||
|
newScrollTop = Math.max(0, Math.min(newScrollTop, maxScrollTop))
|
||||||
|
|
||||||
|
// 计算实际滚动距离
|
||||||
|
const actualDx = newScrollLeft - scrollEl.scrollLeft
|
||||||
|
const actualDy = newScrollTop - scrollEl.scrollTop
|
||||||
|
|
||||||
|
// 应用滚动
|
||||||
|
scrollEl.scrollLeft = newScrollLeft
|
||||||
|
scrollEl.scrollTop = newScrollTop
|
||||||
|
|
||||||
|
// 更新拖拽坐标(只在实际发生滚动时更新)
|
||||||
|
if (actualDx !== 0 || actualDy !== 0) {
|
||||||
|
dragSelection.value.currentX += actualDx
|
||||||
|
dragSelection.value.currentY += actualDy
|
||||||
|
|
||||||
|
// 确保坐标不超出内容范围
|
||||||
|
const maxX = scrollEl.clientWidth + maxScrollLeft
|
||||||
|
const maxY = scrollEl.clientHeight + maxScrollTop
|
||||||
|
dragSelection.value.currentX = Math.max(0, Math.min(dragSelection.value.currentX, maxX))
|
||||||
|
dragSelection.value.currentY = Math.max(0, Math.min(dragSelection.value.currentY, maxY))
|
||||||
|
|
||||||
|
updateDragSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
frameId = requestAnimationFrame(scroll)
|
||||||
}
|
}
|
||||||
|
|
||||||
requestAnimationFrame(scroll)
|
frameId = requestAnimationFrame(scroll)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新框选的项
|
// 更新框选的项
|
||||||
@@ -645,6 +744,11 @@ function updateDragSelection() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleContentClick(event: MouseEvent) {
|
function handleContentClick(event: MouseEvent) {
|
||||||
|
// 如果刚完成拖拽选择,不要清除选择
|
||||||
|
if (dragSelection.value.justFinished) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const target = event.target as HTMLElement
|
const target = event.target as HTMLElement
|
||||||
if (!target.closest('.file-item') && !target.closest('.list-item')) {
|
if (!target.closest('.file-item') && !target.closest('.list-item')) {
|
||||||
if (!event.ctrlKey) {
|
if (!event.ctrlKey) {
|
||||||
@@ -712,8 +816,14 @@ function handleContextMenuAction(action: string, params?: any) {
|
|||||||
// Tooltip 处理
|
// Tooltip 处理
|
||||||
function handleItemMouseEnter(event: MouseEvent, item: FileItem) {
|
function handleItemMouseEnter(event: MouseEvent, item: FileItem) {
|
||||||
if (dragState.value.isDragging) return
|
if (dragState.value.isDragging) return
|
||||||
const tooltip = `名称: ${item.name}\n类型: ${item.type === 'folder' ? '文件夹' : '文件'}\n${item.size ? '大小: ' + item.size : ''}\n${item.date ? '日期: ' + item.date : ''}`
|
|
||||||
tooltipRef.value?.show(tooltip.trim(), event.clientX, event.clientY)
|
const lines: string[] = []
|
||||||
|
lines.push(`类型: ${item.type === 'folder' ? '文件夹' : '文件'}`)
|
||||||
|
if (item.size) lines.push(`大小: ${item.size}`)
|
||||||
|
if (item.date) lines.push(`修改日期: ${item.date}`)
|
||||||
|
|
||||||
|
const tooltip = lines.join('\n')
|
||||||
|
tooltipRef.value?.show(tooltip, event.clientX, event.clientY)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleItemMouseLeave() {
|
function handleItemMouseLeave() {
|
||||||
@@ -893,22 +1003,56 @@ function getGridColumns(): number {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理Ctrl+滚轮缩放
|
||||||
|
function handleWheel(event: WheelEvent) {
|
||||||
|
if (!event.ctrlKey) return
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const delta = event.deltaY > 0 ? -10 : 10
|
||||||
|
const newZoom = Math.max(50, Math.min(200, zoomLevel.value + delta))
|
||||||
|
zoomLevel.value = newZoom
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算动态网格大小
|
||||||
|
const gridItemSize = computed(() => {
|
||||||
|
if (props.viewMode !== 'grid') return {}
|
||||||
|
|
||||||
|
const baseSize = props.thumbnailSize === 'small' ? 80 :
|
||||||
|
props.thumbnailSize === 'medium' ? 100 : 130
|
||||||
|
|
||||||
|
const size = Math.round(baseSize * (zoomLevel.value / 100))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'--grid-item-size': `${size}px`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 监听搜索变化清除选择
|
// 监听搜索变化清除选择
|
||||||
watch(searchQuery, () => {
|
watch(searchQuery, () => {
|
||||||
selection.clearSelection()
|
selection.clearSelection()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 关闭breadcrumb dropdown
|
// 关闭breadcrumb dropdown 和 添加滚轮监听
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.addEventListener('click', () => {
|
const closeDropdown = () => {
|
||||||
breadcrumbDropdown.value.visible = false
|
breadcrumbDropdown.value.visible = false
|
||||||
})
|
}
|
||||||
|
document.addEventListener('click', closeDropdown)
|
||||||
|
|
||||||
|
if (contentAreaRef.value) {
|
||||||
|
contentAreaRef.value.addEventListener('wheel', handleWheel, { passive: false })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener('click', () => {
|
document.removeEventListener('click', () => {
|
||||||
breadcrumbDropdown.value.visible = false
|
breadcrumbDropdown.value.visible = false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (contentAreaRef.value) {
|
||||||
|
contentAreaRef.value.removeEventListener('wheel', handleWheel)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 键盘导航
|
// 键盘导航
|
||||||
@@ -972,6 +1116,8 @@ useKeyboardNav({
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: white;
|
background: white;
|
||||||
transition: opacity 0.15s;
|
transition: opacity 0.15s;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-pane.inactive {
|
.file-pane.inactive {
|
||||||
@@ -1232,6 +1378,7 @@ useKeyboardNav({
|
|||||||
/* Content Area */
|
/* Content Area */
|
||||||
.content-area {
|
.content-area {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -1247,44 +1394,46 @@ useKeyboardNav({
|
|||||||
/* Grid View */
|
/* Grid View */
|
||||||
.file-grid {
|
.file-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 8px;
|
||||||
|
--grid-item-size: 100px; /* 默认值,会被动态覆盖 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-grid.grid-small {
|
.file-grid.grid-small {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(var(--grid-item-size, 80px), 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-grid.grid-medium {
|
.file-grid.grid-medium {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(var(--grid-item-size, 100px), 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-grid.grid-large {
|
.file-grid.grid-large {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(var(--grid-item-size, 130px), 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-item {
|
.file-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px;
|
justify-content: center;
|
||||||
|
padding: 8px;
|
||||||
background: white;
|
background: white;
|
||||||
border: 2px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 8px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-item:hover {
|
.file-item:hover {
|
||||||
background: #f9fafb;
|
background: #f3f4f6;
|
||||||
transform: translateY(-1px);
|
border-color: #d1d5db;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-item.selected {
|
.file-item.selected {
|
||||||
background: #dbeafe;
|
background: #dbeafe;
|
||||||
border-color: #3b82f6;
|
border-color: #3b82f6;
|
||||||
box-shadow: 0 0 0 1px #3b82f6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-item.focused {
|
.file-item.focused {
|
||||||
@@ -1305,16 +1454,23 @@ useKeyboardNav({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.file-icon {
|
.file-icon {
|
||||||
font-size: 32px;
|
font-size: 40px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-name {
|
.file-name {
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
font-weight: 500;
|
|
||||||
color: #374151;
|
color: #374151;
|
||||||
|
line-height: 1.3;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-name :deep(mark) {
|
.file-name :deep(mark) {
|
||||||
@@ -1326,36 +1482,37 @@ useKeyboardNav({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.file-size {
|
.file-size {
|
||||||
font-size: 11px;
|
display: none;
|
||||||
color: #9ca3af;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* List View */
|
/* List View */
|
||||||
.file-list {
|
.file-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1px;
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-item {
|
.list-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 40px 1fr 100px 120px;
|
grid-template-columns: 32px 1fr 100px 140px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px 12px;
|
padding: 4px 8px;
|
||||||
background: white;
|
background: white;
|
||||||
border-bottom: 1px solid #f3f4f6;
|
border: 1px solid transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.15s;
|
transition: background 0.1s;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
min-height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-item:hover {
|
.list-item:hover {
|
||||||
background: #f9fafb;
|
background: #f3f4f6;
|
||||||
|
border-color: #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-item.selected {
|
.list-item.selected {
|
||||||
background: #dbeafe;
|
background: #cce8ff;
|
||||||
|
border-color: #99d1ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-item.drop-target {
|
.list-item.drop-target {
|
||||||
@@ -1370,12 +1527,15 @@ useKeyboardNav({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list-icon {
|
.list-icon {
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-name {
|
.list-name {
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
color: #374151;
|
color: #1f2937;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-name :deep(mark) {
|
.list-name :deep(mark) {
|
||||||
@@ -1387,7 +1547,7 @@ useKeyboardNav({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list-size, .list-date {
|
.list-size, .list-date {
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const content = ref('')
|
|||||||
const position = ref({ x: 0, y: 0 })
|
const position = ref({ x: 0, y: 0 })
|
||||||
const timeout = ref<number>()
|
const timeout = ref<number>()
|
||||||
|
|
||||||
function show(text: string, x: number, y: number, delay = 500) {
|
function show(text: string, x: number, y: number, delay = 1000) {
|
||||||
clearTimeout(timeout.value)
|
clearTimeout(timeout.value)
|
||||||
|
|
||||||
timeout.value = window.setTimeout(() => {
|
timeout.value = window.setTimeout(() => {
|
||||||
@@ -42,16 +42,17 @@ defineExpose({
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.tooltip {
|
.tooltip {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background: rgba(0, 0, 0, 0.85);
|
background: white;
|
||||||
color: white;
|
color: #374151;
|
||||||
padding: 6px 10px;
|
padding: 8px 12px;
|
||||||
border-radius: 6px;
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
white-space: nowrap;
|
white-space: pre-line;
|
||||||
max-width: 300px;
|
max-width: 280px;
|
||||||
word-wrap: break-word;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
white-space: normal;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -5,103 +5,126 @@ export const mockFolderStructure: FolderStructure = {
|
|||||||
Home: {
|
Home: {
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
type: 'folder',
|
type: 'folder',
|
||||||
children: {
|
children: (() => {
|
||||||
'My Photos': {
|
const homeItems: Record<string, any> = {
|
||||||
name: 'My Photos',
|
'My Photos': {
|
||||||
type: 'folder',
|
name: 'My Photos',
|
||||||
children: {
|
type: 'folder',
|
||||||
'Vacation 2023': {
|
children: {
|
||||||
name: 'Vacation 2023',
|
'Vacation 2023': {
|
||||||
type: 'folder',
|
name: 'Vacation 2023',
|
||||||
children: {
|
type: 'folder',
|
||||||
Beach: {
|
children: {
|
||||||
name: 'Beach',
|
Beach: {
|
||||||
type: 'folder',
|
name: 'Beach',
|
||||||
children: {
|
type: 'folder',
|
||||||
'photo1.jpg': { name: 'photo1.jpg', type: 'photo', size: '2.4 MB', date: '2023-07-15' },
|
children: {
|
||||||
'photo2.jpg': { name: 'photo2.jpg', type: 'photo', size: '3.1 MB', date: '2023-07-15' },
|
'photo1.jpg': { name: 'photo1.jpg', type: 'photo', size: '2.4 MB', date: '2023-07-15' },
|
||||||
'photo3.jpg': { name: 'photo3.jpg', type: 'photo', size: '2.8 MB', date: '2023-07-16' },
|
'photo2.jpg': { name: 'photo2.jpg', type: 'photo', size: '3.1 MB', date: '2023-07-15' },
|
||||||
'beach_sunset.jpg': { name: 'beach_sunset.jpg', type: 'photo', size: '5.2 MB', date: '2023-07-17' },
|
'photo3.jpg': { name: 'photo3.jpg', type: 'photo', size: '2.8 MB', date: '2023-07-16' },
|
||||||
'waves.jpg': { name: 'waves.jpg', type: 'photo', size: '3.8 MB', date: '2023-07-18' }
|
'beach_sunset.jpg': { name: 'beach_sunset.jpg', type: 'photo', size: '5.2 MB', date: '2023-07-17' },
|
||||||
}
|
'waves.jpg': { name: 'waves.jpg', type: 'photo', size: '3.8 MB', date: '2023-07-18' }
|
||||||
},
|
}
|
||||||
Mountains: {
|
},
|
||||||
name: 'Mountains',
|
Mountains: {
|
||||||
type: 'folder',
|
name: 'Mountains',
|
||||||
children: {
|
type: 'folder',
|
||||||
'sunrise.jpg': { name: 'sunrise.jpg', type: 'photo', size: '4.2 MB', date: '2023-07-20' },
|
children: {
|
||||||
'peak_view.jpg': { name: 'peak_view.jpg', type: 'photo', size: '6.1 MB', date: '2023-07-21' }
|
'sunrise.jpg': { name: 'sunrise.jpg', type: 'photo', size: '4.2 MB', date: '2023-07-20' },
|
||||||
|
'peak_view.jpg': { name: 'peak_view.jpg', type: 'photo', size: '6.1 MB', date: '2023-07-21' }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
Family: {
|
|
||||||
name: 'Family',
|
|
||||||
type: 'folder',
|
|
||||||
children: {
|
|
||||||
'dinner.jpg': { name: 'dinner.jpg', type: 'photo', size: '2.2 MB', date: '2023-08-15' },
|
|
||||||
'family_portrait.jpg': { name: 'family_portrait.jpg', type: 'photo', size: '4.1 MB', date: '2023-08-20' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'Camera Roll': {
|
|
||||||
name: 'Camera Roll',
|
|
||||||
type: 'folder',
|
|
||||||
children: (() => {
|
|
||||||
const items: Record<string, any> = {
|
|
||||||
Recent: {
|
|
||||||
name: 'Recent',
|
|
||||||
type: 'folder',
|
|
||||||
children: {}
|
|
||||||
},
|
},
|
||||||
Favorites: {
|
Family: {
|
||||||
name: 'Favorites',
|
name: 'Family',
|
||||||
type: 'folder',
|
type: 'folder',
|
||||||
children: {}
|
children: {
|
||||||
|
'dinner.jpg': { name: 'dinner.jpg', type: 'photo', size: '2.2 MB', date: '2023-08-15' },
|
||||||
|
'family_portrait.jpg': { name: 'family_portrait.jpg', type: 'photo', size: '4.1 MB', date: '2023-08-20' }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
// 生成100张照片
|
'Camera Roll': {
|
||||||
for (let i = 1; i <= 100; i++) {
|
name: 'Camera Roll',
|
||||||
const size = (Math.random() * 3 + 1).toFixed(1)
|
type: 'folder',
|
||||||
const day = Math.floor(Math.random() * 30) + 1
|
children: (() => {
|
||||||
items[`IMG_${String(i).padStart(4, '0')}.jpg`] = {
|
const items: Record<string, any> = {
|
||||||
name: `IMG_${String(i).padStart(4, '0')}.jpg`,
|
Recent: {
|
||||||
type: 'photo',
|
name: 'Recent',
|
||||||
size: `${size} MB`,
|
type: 'folder',
|
||||||
date: `2023-09-${String(day).padStart(2, '0')}`
|
children: {}
|
||||||
|
},
|
||||||
|
Favorites: {
|
||||||
|
name: 'Favorites',
|
||||||
|
type: 'folder',
|
||||||
|
children: {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Recent文件夹添加照片
|
// 生成100张照片
|
||||||
for (let i = 1; i <= 30; i++) {
|
for (let i = 1; i <= 100; i++) {
|
||||||
const size = (Math.random() * 3 + 1).toFixed(1)
|
const size = (Math.random() * 3 + 1).toFixed(1)
|
||||||
items.Recent.children[`Recent_${String(i).padStart(3, '0')}.jpg`] = {
|
const day = Math.floor(Math.random() * 30) + 1
|
||||||
name: `Recent_${String(i).padStart(3, '0')}.jpg`,
|
items[`IMG_${String(i).padStart(4, '0')}.jpg`] = {
|
||||||
type: 'photo',
|
name: `IMG_${String(i).padStart(4, '0')}.jpg`,
|
||||||
size: `${size} MB`,
|
type: 'photo',
|
||||||
date: `2023-12-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`
|
size: `${size} MB`,
|
||||||
|
date: `2023-09-${String(day).padStart(2, '0')}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return items
|
// Recent文件夹添加照片
|
||||||
})()
|
for (let i = 1; i <= 30; i++) {
|
||||||
},
|
const size = (Math.random() * 3 + 1).toFixed(1)
|
||||||
'Work Projects': {
|
items.Recent.children[`Recent_${String(i).padStart(3, '0')}.jpg`] = {
|
||||||
name: 'Work Projects',
|
name: `Recent_${String(i).padStart(3, '0')}.jpg`,
|
||||||
type: 'folder',
|
type: 'photo',
|
||||||
children: {
|
size: `${size} MB`,
|
||||||
Screenshots: {
|
date: `2023-12-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`
|
||||||
name: 'Screenshots',
|
}
|
||||||
type: 'folder',
|
}
|
||||||
children: {
|
|
||||||
'design1.png': { name: 'design1.png', type: 'photo', size: '1.2 MB', date: '2023-09-01' },
|
return items
|
||||||
'design2.png': { name: 'design2.png', type: 'photo', size: '1.5 MB', date: '2023-09-02' }
|
})()
|
||||||
|
},
|
||||||
|
'Work Projects': {
|
||||||
|
name: 'Work Projects',
|
||||||
|
type: 'folder',
|
||||||
|
children: {
|
||||||
|
Screenshots: {
|
||||||
|
name: 'Screenshots',
|
||||||
|
type: 'folder',
|
||||||
|
children: {
|
||||||
|
'design1.png': { name: 'design1.png', type: 'photo', size: '1.2 MB', date: '2023-09-01' },
|
||||||
|
'design2.png': { name: 'design2.png', type: 'photo', size: '1.5 MB', date: '2023-09-02' }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// 在Home文件夹添加更多文件夹和文件,确保有足够内容触发滚动
|
||||||
|
for (let i = 1; i <= 50; i++) {
|
||||||
|
homeItems[`Folder_${String(i).padStart(2, '0')}`] = {
|
||||||
|
name: `Folder_${String(i).padStart(2, '0')}`,
|
||||||
|
type: 'folder',
|
||||||
|
children: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i <= 50; i++) {
|
||||||
|
const size = (Math.random() * 5 + 1).toFixed(1)
|
||||||
|
homeItems[`File_${String(i).padStart(2, '0')}.jpg`] = {
|
||||||
|
name: `File_${String(i).padStart(2, '0')}.jpg`,
|
||||||
|
type: 'photo',
|
||||||
|
size: `${size} MB`,
|
||||||
|
date: `2024-01-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return homeItems
|
||||||
|
})()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,15 +298,13 @@ const handleMenuSelect = (key: string) => {
|
|||||||
|
|
||||||
.layout-content {
|
.layout-content {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background-color: var(--content-bg);
|
background-color: var(--content-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-wrapper {
|
.content-wrapper {
|
||||||
padding: 24px;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: calc(100vh - 64px);
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import FileManager from '@/components/FileManager/FileManager.vue'
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.field-view {
|
.field-view {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in new issue
Block a user