diff --git a/resources/assets/js/pages/manage/file.vue b/resources/assets/js/pages/manage/file.vue index b26f74e1d..2c456b965 100644 --- a/resources/assets/js/pages/manage/file.vue +++ b/resources/assets/js/pages/manage/file.vue @@ -118,7 +118,16 @@

{{$L('没有任何文件')}}

-
+
+
@@ -574,6 +587,17 @@ export default { pasteShow: false, pasteFile: [], pasteItem: [], + + dragSelecting: false, + dragSelectStart: null, + dragSelectRect: null, + dragSelectStyle: {}, + dragSelectBase: [], + dragSelectPreserve: false, + dragSelectContainerSize: null, + dragSelectPointerId: null, + dragSelectMoved: false, + dragSelectPreventClick: false, } }, @@ -785,6 +809,14 @@ export default { this.getFileList(); }, + deactivated() { + this.cancelDragSelection(); + }, + + beforeDestroy() { + this.cancelDragSelection(); + }, + computed: { ...mapState([ 'systemConfig', @@ -939,6 +971,9 @@ export default { tableMode(val) { $A.IDBSave("fileTableMode", val) + if (val === 'table') { + this.cancelDragSelection(); + } }, hideShared(val) { @@ -1073,6 +1108,223 @@ export default { }) }, + onFileListPointerDown(event) { + if (this.tableMode === 'table') { + return; + } + const isPrimaryButton = event.button === 0 || event.pointerType === 'touch'; + if (!isPrimaryButton) { + return; + } + const container = this.$refs.blockFileList; + if (!container) { + return; + } + let element = event.target; + let onFileItem = false; + while (element && element !== container) { + if (element.classList) { + if (element.classList.contains('file-menu') || element.classList.contains('file-check') || element.tagName === 'INPUT' || element.tagName === 'BUTTON') { + return; + } + } + if (element.classList && element.classList.contains('file-item')) { + onFileItem = true; + break; + } + element = element.parentNode; + } + if (onFileItem) { + return; + } + this.dragSelectMoved = false; + this.dragSelectPreventClick = false; + if (this.contextMenuVisible) { + this.handleClickContextMenuOutside(); + } + const containerRect = container.getBoundingClientRect(); + const scrollLeft = container.scrollLeft; + const scrollTop = container.scrollTop; + const start = { + x: event.clientX - containerRect.left + scrollLeft, + y: event.clientY - containerRect.top + scrollTop, + }; + this.dragSelecting = true; + this.dragSelectStart = start; + this.dragSelectRect = { + left: start.x, + top: start.y, + width: 0, + height: 0, + }; + this.dragSelectStyle = { + left: `${start.x - scrollLeft}px`, + top: `${start.y - scrollTop}px`, + width: '0px', + height: '0px', + }; + this.dragSelectContainerSize = { + width: container.scrollWidth, + height: container.scrollHeight, + }; + this.dragSelectPreserve = event.ctrlKey || event.metaKey; + this.dragSelectBase = this.dragSelectPreserve ? this.selectedItems.map(item => ({...item})) : []; + if (!this.dragSelectPreserve && this.selectedItems.length > 0) { + this.selectedItems = []; + } + if (event.pointerId !== undefined) { + try { + container.setPointerCapture(event.pointerId); + this.dragSelectPointerId = event.pointerId; + } catch (e) {} + } + event.preventDefault(); + }, + + onFileListPointerMove(event) { + if (!this.dragSelecting || !this.dragSelectStart) { + return; + } + event.preventDefault(); + const container = this.$refs.blockFileList; + if (!container) { + return; + } + const containerRect = container.getBoundingClientRect(); + const scrollLeft = container.scrollLeft; + const scrollTop = container.scrollTop; + const sizeInfo = this.dragSelectContainerSize || { + width: container.scrollWidth, + height: container.scrollHeight, + }; + const currentX = Math.min(Math.max(event.clientX - containerRect.left + scrollLeft, 0), sizeInfo.width); + const currentY = Math.min(Math.max(event.clientY - containerRect.top + scrollTop, 0), sizeInfo.height); + const start = this.dragSelectStart; + const left = Math.min(start.x, currentX); + const top = Math.min(start.y, currentY); + const width = Math.abs(start.x - currentX); + const height = Math.abs(start.y - currentY); + const rect = {left, top, width, height}; + this.dragSelectRect = rect; + this.dragSelectStyle = { + left: `${left - scrollLeft}px`, + top: `${top - scrollTop}px`, + width: `${width}px`, + height: `${height}px`, + }; + if (!this.dragSelectMoved && (width > 3 || height > 3)) { + this.dragSelectMoved = true; + } + this.updateDragSelection(rect); + }, + + onFileListPointerUp() { + if (this.dragSelecting && this.dragSelectRect) { + this.updateDragSelection(this.dragSelectRect); + } + const moved = this.dragSelectMoved; + this.cancelDragSelection(); + if (moved) { + this.dragSelectPreventClick = true; + setTimeout(() => { + this.dragSelectPreventClick = false; + }, 0); + } + }, + + onFileListPointerLeave(event) { + if (!this.dragSelecting) { + return; + } + if (event.pointerId !== undefined && this.dragSelectPointerId !== event.pointerId) { + return; + } + this.onFileListPointerUp(); + }, + + updateDragSelection(rect) { + const container = this.$refs.blockFileList; + if (!container || !rect) { + return; + } + const containerRect = container.getBoundingClientRect(); + const scrollLeft = container.scrollLeft; + const scrollTop = container.scrollTop; + const selectionBounds = { + left: rect.left, + top: rect.top, + right: rect.left + rect.width, + bottom: rect.top + rect.height, + }; + const fileMap = new Map(this.fileList.map(file => [String(file.id), file])); + const next = []; + const seen = new Set(); + + this.dragSelectBase.forEach(item => { + const key = String(item.id); + if (!seen.has(key)) { + seen.add(key); + next.push(item); + } + }); + + Array.from(container.querySelectorAll('.file-item')).forEach(el => { + const key = el.dataset ? el.dataset.id || el.getAttribute('data-id') : el.getAttribute('data-id'); + if (!key || seen.has(String(key))) { + return; + } + const elRect = el.getBoundingClientRect(); + const bounds = { + left: elRect.left - containerRect.left + scrollLeft, + top: elRect.top - containerRect.top + scrollTop, + right: elRect.right - containerRect.left + scrollLeft, + bottom: elRect.bottom - containerRect.top + scrollTop, + }; + if (!this.rectsIntersect(bounds, selectionBounds)) { + return; + } + const file = fileMap.get(String(key)); + if (!file) { + return; + } + seen.add(String(key)); + next.push({ + id: file.id, + name: file.name, + type: file.type, + size: file.size, + }); + }); + + this.selectedItems = next; + }, + + rectsIntersect(a, b) { + if (!a || !b) { + return false; + } + return !(a.right < b.left || a.left > b.right || a.bottom < b.top || a.top > b.bottom); + }, + + cancelDragSelection() { + this.dragSelecting = false; + this.dragSelectStart = null; + this.dragSelectRect = null; + this.dragSelectStyle = {}; + this.dragSelectBase = []; + this.dragSelectPreserve = false; + this.dragSelectContainerSize = null; + const container = this.$refs.blockFileList; + if (container && this.dragSelectPointerId !== null && container.hasPointerCapture && container.hasPointerCapture(this.dragSelectPointerId)) { + try { + container.releasePointerCapture(this.dragSelectPointerId); + } catch (e) {} + } + this.dragSelectPointerId = null; + this.dragSelectMoved = false; + this.dragSelectPreventClick = false; + }, + handleContextmenu(event) { if (this.windowLandscape) { this.handleRightClick(event) @@ -1217,6 +1469,9 @@ export default { }, dropFile(item, command) { + if (this.dragSelectPreventClick && ['open', 'openCheckMenu', 'select'].includes(command)) { + return; + } switch (command) { case 'open': case 'openCheckMenu': diff --git a/resources/assets/sass/pages/page-file.scss b/resources/assets/sass/pages/page-file.scss index 36ee315cc..05e7d5609 100644 --- a/resources/assets/sass/pages/page-file.scss +++ b/resources/assets/sass/pages/page-file.scss @@ -350,6 +350,7 @@ } .file-list { flex: 1; + position: relative; padding: 0 20px 20px; margin-top: 16px; overflow: auto; @@ -487,6 +488,13 @@ } } } + .file-drag-select { + position: absolute; + border: 1px dashed #0bc037; + background-color: rgba(10, 191, 56, 0.12); + pointer-events: none; + z-index: 2; + } } .drag-over { position: absolute;