feat: 文件管理支持多标签页

This commit is contained in:
耗子
2026-02-11 20:12:10 +08:00
parent de5d76bff6
commit 2dd6f0d9f0
9 changed files with 527 additions and 278 deletions
+150 -20
View File
@@ -1,40 +1,158 @@
export interface File {
path: string
keyword: string
sub: boolean
import type { Marked } from '@/views/file/types'
export interface FileTab {
id: string // 标签页唯一标识
label: string // 显示名(路径末级目录名)
path: string // 当前路径
keyword: string // 搜索关键词
sub: boolean // 搜索是否包含子目录
history: string[] // 浏览历史栈
historyCursor: number // 历史指针
}
export interface FileState {
tabs: FileTab[]
activeTabId: string
// 全局偏好(跨标签页共享)
showHidden: boolean
viewType: 'list' | 'grid'
sortKey: string
sortOrder: 'asc' | 'desc'
// 全局剪贴板(跨标签页共享)
clipboard: {
marked: Marked[]
markedType: 'copy' | 'move'
}
}
// 最大标签页数量
const MAX_TABS = 10
// 根据路径生成标签页显示名
const getLabelFromPath = (path: string): string => {
if (path === '/') return '/'
return path.split('/').pop() || '/'
}
// 创建新标签页
const createNewTab = (path: string): FileTab => ({
id: crypto.randomUUID(),
label: getLabelFromPath(path),
path,
keyword: '',
sub: false,
history: [path],
historyCursor: 0
})
export const useFileStore = defineStore('file', {
state: (): File => {
state: (): FileState => {
const initialTab = createNewTab('/opt')
return {
path: '/opt',
keyword: '',
sub: false,
tabs: [initialTab],
activeTabId: initialTab.id,
showHidden: false,
viewType: 'list',
sortKey: '',
sortOrder: 'asc'
sortOrder: 'asc',
clipboard: {
marked: [],
markedType: 'copy'
}
}
},
getters: {
sort(): string {
if (!this.sortKey) return ''
return this.sortOrder === 'desc' ? `-${this.sortKey}` : this.sortKey
},
activeTab(): FileTab | undefined {
return this.tabs.find((t) => t.id === this.activeTabId)
}
},
actions: {
set(info: File) {
this.path = info.path
this.keyword = info.keyword
this.sub = info.sub
this.showHidden = info.showHidden
this.viewType = info.viewType
this.sortKey = info.sortKey
this.sortOrder = info.sortOrder
// 新建标签页
createTab(path?: string) {
if (this.tabs.length >= MAX_TABS) {
window.$message.warning('标签页数量已达上限')
return
}
const tabPath = path ?? this.activeTab?.path ?? '/opt'
const tab = createNewTab(tabPath)
this.tabs.push(tab)
this.activeTabId = tab.id
},
// 关闭标签页
closeTab(tabId: string) {
if (this.tabs.length <= 1) return
const index = this.tabs.findIndex((t) => t.id === tabId)
if (index === -1) return
this.tabs.splice(index, 1)
// 如果关闭的是当前活跃标签页,切换到相邻标签页
if (this.activeTabId === tabId) {
const newIndex = Math.min(index, this.tabs.length - 1)
this.activeTabId = this.tabs[newIndex]!.id
}
},
// 切换标签页
switchTab(tabId: string) {
if (this.tabs.some((t) => t.id === tabId)) {
this.activeTabId = tabId
}
},
// 更新标签页路径
updateTabPath(tabId: string, path: string) {
const tab = this.tabs.find((t) => t.id === tabId)
if (!tab) return
tab.path = path
tab.label = getLabelFromPath(path)
tab.keyword = ''
tab.sub = false
this.pushHistory(tabId, path)
},
// 推入历史记录
pushHistory(tabId: string, path: string) {
const tab = this.tabs.find((t) => t.id === tabId)
if (!tab) return
// 如果当前位置就是这个路径,不重复推入
if (tab.history[tab.historyCursor] === path) return
// 截断 cursor 后的 future
tab.history.splice(tab.historyCursor + 1)
tab.history.push(path)
tab.historyCursor = tab.history.length - 1
},
// 历史后退
historyBack(tabId: string) {
const tab = this.tabs.find((t) => t.id === tabId)
if (!tab || tab.historyCursor <= 0) return
tab.historyCursor--
tab.path = tab.history[tab.historyCursor] ?? '/'
tab.label = getLabelFromPath(tab.path)
tab.keyword = ''
tab.sub = false
},
// 历史前进
historyForward(tabId: string) {
const tab = this.tabs.find((t) => t.id === tabId)
if (!tab || tab.historyCursor >= tab.history.length - 1) return
tab.historyCursor++
tab.path = tab.history[tab.historyCursor] ?? '/'
tab.label = getLabelFromPath(tab.path)
tab.keyword = ''
tab.sub = false
},
// 重新排序标签页(拖拽)
reorderTabs(tabs: FileTab[]) {
this.tabs = tabs
},
// 设置剪贴板
setClipboard(marked: Marked[], markedType: 'copy' | 'move') {
this.clipboard.marked = marked
this.clipboard.markedType = markedType
},
// 清空剪贴板
clearClipboard() {
this.clipboard.marked = []
},
toggleShowHidden() {
this.showHidden = !this.showHidden
@@ -44,7 +162,6 @@ export const useFileStore = defineStore('file', {
},
setSort(key: string) {
if (this.sortKey === key) {
// 同一列:切换排序方向,或取消排序
if (this.sortOrder === 'asc') {
this.sortOrder = 'desc'
} else {
@@ -52,11 +169,24 @@ export const useFileStore = defineStore('file', {
this.sortOrder = 'asc'
}
} else {
// 不同列:设置新的排序列
this.sortKey = key
this.sortOrder = 'asc'
}
}
},
persist: true
persist: {
afterHydrate(ctx: any) {
const store = ctx.store as ReturnType<typeof useFileStore>
// 恢复后清空剪贴板
store.clipboard = { marked: [], markedType: 'copy' }
// 确保 activeTabId 有效
if (!store.tabs || store.tabs.length === 0) {
const tab = createNewTab('/opt')
store.tabs = [tab]
store.activeTabId = tab.id
} else if (!store.tabs.some((t) => t.id === store.activeTabId)) {
store.activeTabId = store.tabs[0]!.id
}
}
}
})
+1 -1
View File
@@ -72,7 +72,7 @@ const columns: any = [
class: 'cursor-pointer hover:opacity-60',
type: 'info',
onClick: () => {
fileStore.path = row.path
fileStore.activeTab && fileStore.updateTabPath(fileStore.activeTabId, row.path)
router.push({ name: 'file-index' })
}
},
+223 -35
View File
@@ -3,20 +3,23 @@ defineOptions({
name: 'file-index'
})
import { useThemeVars } from 'naive-ui'
import draggable from 'vuedraggable'
import { useFileStore } from '@/store'
import type { FileTab } from '@/store/modules/file'
import CompressModal from '@/views/file/CompressModal.vue'
import ListView from '@/views/file/ListView.vue'
import PathInput from '@/views/file/PathInput.vue'
import PermissionModal from '@/views/file/PermissionModal.vue'
import ToolBar from '@/views/file/ToolBar.vue'
import UploadModal from '@/views/file/UploadModal.vue'
import type { FileInfo, Marked } from '@/views/file/types'
import type { FileInfo } from '@/views/file/types'
const fileStore = useFileStore()
const themeVars = useThemeVars()
const selected = ref<string[]>([])
const marked = ref<Marked[]>([])
const markedType = ref<string>('copy')
// 权限编辑时的文件信息列表
const permissionFileInfoList = ref<FileInfo[]>([])
@@ -28,11 +31,24 @@ const upload = ref(false)
const droppedFiles = ref<File[]>([])
const isDragging = ref(false)
// 拖拽排序用的本地副本
const localTabs = computed({
get: () => fileStore.tabs,
set: (val: FileTab[]) => fileStore.reorderTabs(val)
})
// 切换标签页时清空选中
watch(
() => fileStore.activeTabId,
() => {
selected.value = []
}
)
// 处理拖拽进入
const handleDragEnter = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
// 检查是否有文件
if (e.dataTransfer?.types.includes('Files')) {
isDragging.value = true
}
@@ -42,7 +58,6 @@ const handleDragEnter = (e: DragEvent) => {
const handleDragLeave = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
// 只有当离开整个容器时才隐藏
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
if (
e.clientX <= rect.left ||
@@ -75,7 +90,6 @@ const readDirectoryRecursively = async (
}
let entries: FileSystemEntry[] = []
// readEntries 可能需要多次调用才能获取所有条目
let batch: FileSystemEntry[]
do {
batch = await readEntries()
@@ -88,7 +102,6 @@ const readDirectoryRecursively = async (
const fileEntry = childEntry as FileSystemFileEntry
const file = await new Promise<File>((resolve, reject) => {
fileEntry.file((f) => {
// 创建带有相对路径的新 File 对象
const newFile = new File([f], childPath, { type: f.type, lastModified: f.lastModified })
resolve(newFile)
}, reject)
@@ -117,7 +130,6 @@ const handleDrop = async (e: DragEvent) => {
const files: File[] = []
// 使用 webkitGetAsEntry 来支持文件夹
for (let i = 0; i < items.length; i++) {
const item = items[i]
if (item?.kind === 'file') {
@@ -149,6 +161,25 @@ watch(upload, (val) => {
droppedFiles.value = []
}
})
// 中键点击标签页关闭
const handleTabMouseDown = (e: MouseEvent, tabId: string) => {
if (e.button === 1) {
e.preventDefault()
fileStore.closeTab(tabId)
}
}
// 主题变量映射到 CSS
const tabStyles = computed(() => ({
'--tab-bg': themeVars.value.cardColor,
'--tab-bg-hover': themeVars.value.hoverColor,
'--tab-border': themeVars.value.borderColor,
'--tab-text': themeVars.value.textColor2,
'--tab-text-active': themeVars.value.textColor1,
'--tab-text-muted': themeVars.value.textColor3,
'--tab-primary': themeVars.value.primaryColor
}))
</script>
<template>
@@ -161,34 +192,66 @@ watch(upload, (val) => {
@drop="handleDrop"
>
<n-flex vertical :size="20" class="flex-1 min-h-0">
<path-input
v-model:path="fileStore.path"
v-model:keyword="fileStore.keyword"
v-model:sub="fileStore.sub"
/>
<tool-bar
v-model:path="fileStore.path"
v-model:selected="selected"
v-model:marked="marked"
v-model:markedType="markedType"
v-model:compress="compress"
v-model:permission="permission"
v-model:upload="upload"
/>
<list-view
v-model:path="fileStore.path"
v-model:keyword="fileStore.keyword"
v-model:sub="fileStore.sub"
v-model:selected="selected"
v-model:marked="marked"
v-model:markedType="markedType"
v-model:compress="compress"
v-model:permission="permission"
v-model:permission-file-info-list="permissionFileInfoList"
/>
<!-- 标签页栏 -->
<div class="file-tabs" :style="tabStyles">
<draggable
v-model="localTabs"
item-key="id"
class="file-tabs-list"
:animation="200"
ghost-class="file-tab-ghost"
drag-class="file-tab-drag"
>
<template #item="{ element: tab }">
<div
class="file-tab"
:class="{ active: tab.id === fileStore.activeTabId }"
@click="fileStore.switchTab(tab.id)"
@mousedown="handleTabMouseDown($event, tab.id)"
>
<i-mdi-folder-outline class="file-tab-icon" />
<span class="file-tab-label" :title="tab.path">{{ tab.label }}</span>
<span
v-if="fileStore.tabs.length > 1"
class="file-tab-close"
@click.stop="fileStore.closeTab(tab.id)"
>
<i-mdi-close :size="14" />
</span>
</div>
</template>
<template #footer>
<div class="file-tab-add" @click="fileStore.createTab()">
<i-mdi-plus :size="16" />
</div>
</template>
</draggable>
</div>
<!-- 每个标签页内容v-if 只渲染活跃的 -->
<template v-for="tab in fileStore.tabs" :key="tab.id">
<template v-if="tab.id === fileStore.activeTabId">
<path-input :tab-id="tab.id" />
<tool-bar
:tab-id="tab.id"
v-model:selected="selected"
v-model:compress="compress"
v-model:permission="permission"
v-model:upload="upload"
/>
<list-view
:tab-id="tab.id"
v-model:selected="selected"
v-model:compress="compress"
v-model:permission="permission"
v-model:permission-file-info-list="permissionFileInfoList"
/>
</template>
</template>
<compress-modal
v-model:show="compress"
v-model:path="fileStore.path"
v-model:path="fileStore.activeTab!.path"
v-model:selected="selected"
/>
<permission-modal
@@ -209,7 +272,7 @@ watch(upload, (val) => {
<!-- 上传弹窗 -->
<upload-modal
v-model:show="upload"
v-model:path="fileStore.path"
v-model:path="fileStore.activeTab!.path"
:initial-files="droppedFiles"
/>
</common-page>
@@ -235,4 +298,129 @@ watch(upload, (val) => {
color: white;
font-size: 18px;
}
.file-tabs {
flex-shrink: 0;
margin-bottom: -8px;
border-bottom: 1px solid var(--tab-border);
}
.file-tabs-list {
display: flex;
align-items: stretch;
overflow-x: auto;
padding: 0;
// 隐藏滚动条
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.file-tab {
display: flex;
align-items: center;
gap: 6px;
padding: 7px 12px;
font-size: 13px;
cursor: pointer;
white-space: nowrap;
user-select: none;
color: var(--tab-text);
transition: all 0.15s ease;
position: relative;
min-width: 0;
&:hover {
background: var(--tab-bg-hover);
color: var(--tab-text-active);
.file-tab-close {
opacity: 1;
}
}
&.active {
background: var(--tab-bg);
color: var(--tab-text-active);
font-weight: 500;
// 底部 primary 色指示条
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: var(--tab-primary);
}
.file-tab-close {
opacity: 0.6;
}
}
}
.file-tab-icon {
font-size: 15px;
flex-shrink: 0;
opacity: 0.6;
}
.file-tab-label {
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
}
.file-tab-close {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 18px;
height: 18px;
border-radius: 4px;
opacity: 0;
color: var(--tab-text-muted);
transition: all 0.1s ease;
&:hover {
background: rgba(0, 0, 0, 0.12);
color: var(--tab-text-active);
opacity: 1 !important;
}
}
.file-tab-add {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
margin: auto 0;
border-radius: 6px;
cursor: pointer;
color: var(--tab-text-muted);
flex-shrink: 0;
transition: all 0.15s ease;
&:hover {
background: var(--tab-bg-hover);
color: var(--tab-text-active);
}
}
// 拖拽时的幽灵元素
.file-tab-ghost {
opacity: 0.4;
}
// 拖拽中的元素
.file-tab-drag {
background: var(--tab-bg) !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
</style>
+37 -87
View File
@@ -26,25 +26,36 @@ import {
isCompress,
isImage
} from '@/utils/file'
import { usePaste } from '@/views/file/composables/usePaste'
import EditModal from '@/views/file/EditModal.vue'
import PreviewModal from '@/views/file/PreviewModal.vue'
import PropertyModal from '@/views/file/PropertyModal.vue'
import type { FileInfo, Marked } from '@/views/file/types'
import type { FileInfo } from '@/views/file/types'
import copy2clipboard from '@vavt/copy2clipboard'
const { $gettext } = useGettext()
const themeVars = useThemeVars()
const fileStore = useFileStore()
const { handlePaste: doPaste } = usePaste()
const props = defineProps<{
tabId: string
}>()
// 排序状态
const sort = computed(() => fileStore.sort)
const path = defineModel<string>('path', { type: String, required: true })
const keyword = defineModel<string>('keyword', { type: String, default: '' })
const sub = defineModel<boolean>('sub', { type: Boolean, default: false })
const tab = computed(() => fileStore.tabs.find((t) => t.id === props.tabId)!)
const path = computed({
get: () => tab.value.path,
set: (v: string) => fileStore.updateTabPath(props.tabId, v)
})
const keyword = computed(() => tab.value.keyword)
const sub = computed(() => tab.value.sub)
const marked = computed(() => fileStore.clipboard.marked)
const markedType = computed(() => fileStore.clipboard.markedType)
const selected = defineModel<any[]>('selected', { type: Array, default: () => [] })
const marked = defineModel<Marked[]>('marked', { type: Array, default: () => [] })
const markedType = defineModel<string>('markedType', { type: String, required: true })
const compress = defineModel<boolean>('compress', { type: Boolean, required: true })
const permission = defineModel<boolean>('permission', { type: Boolean, required: true })
const permissionFileInfoList = defineModel<FileInfo[]>('permissionFileInfoList', {
@@ -636,12 +647,14 @@ const getSelectedItems = () => {
// 标记文件(复制/移动)
const markFiles = (items: any[], type: 'copy' | 'move') => {
marked.value = items.map((item: any) => ({
name: item.name,
source: item.full,
force: false
}))
markedType.value = type
fileStore.setClipboard(
items.map((item: any) => ({
name: item.name,
source: item.full,
force: false
})),
type
)
window.$message.success(
$gettext('Marked successfully, please navigate to the destination path to paste')
)
@@ -826,77 +839,7 @@ const copyPath = (item: any) => {
// ==================== 处理粘贴 ====================
const handlePaste = () => {
if (!marked.value.length) {
window.$message.error($gettext('Please mark the files/folders to copy or move first'))
return
}
let flag = false
const paths = marked.value.map((item) => ({
name: item.name,
source: item.source,
target: path.value + '/' + item.name,
force: false
}))
const sources = paths.map((item: any) => item.target)
useRequest(file.exist(sources)).onSuccess(({ data }) => {
for (let i = 0; i < data.length; i++) {
if (data[i]) {
flag = true
const pathItem = paths[i]
if (pathItem) pathItem.force = true
}
}
if (flag) {
window.$dialog.warning({
title: $gettext('Warning'),
content: $gettext(
'There are items with the same name %{ items } Do you want to overwrite?',
{
items: `${paths
.filter((item) => item.force)
.map((item) => item.name)
.join(', ')}`
}
),
positiveText: $gettext('Overwrite'),
negativeText: $gettext('Cancel'),
onPositiveClick: () => {
if (markedType.value == 'copy') {
useRequest(file.copy(paths)).onSuccess(() => {
marked.value = []
window.$bus.emit('file:refresh')
window.$message.success($gettext('Copied successfully'))
})
} else {
useRequest(file.move(paths)).onSuccess(() => {
marked.value = []
window.$bus.emit('file:refresh')
window.$message.success($gettext('Moved successfully'))
})
}
},
onNegativeClick: () => {
marked.value = []
window.$message.info($gettext('Canceled'))
}
})
} else {
if (markedType.value == 'copy') {
useRequest(file.copy(paths)).onSuccess(() => {
marked.value = []
window.$bus.emit('file:refresh')
window.$message.success($gettext('Copied successfully'))
})
} else {
useRequest(file.move(paths)).onSuccess(() => {
marked.value = []
window.$bus.emit('file:refresh')
window.$message.success($gettext('Moved successfully'))
})
}
}
})
doPaste(path.value)
}
const handleSelect = (key: string) => {
@@ -1135,6 +1078,16 @@ const handleKeyDown = (event: KeyboardEvent) => {
handlePaste()
}
break
case 't':
// Ctrl/Cmd + T: 新建标签页
event.preventDefault()
fileStore.createTab()
break
case 'w':
// Ctrl/Cmd + W: 关闭当前标签页
event.preventDefault()
fileStore.closeTab(props.tabId)
break
}
} else {
const currentIndex = getSelectedIndex()
@@ -1277,7 +1230,7 @@ const handleFileSearch = () => {
nextTick(() => {
refresh()
})
window.$bus.emit('file:push-history', path.value)
fileStore.pushHistory(props.tabId, path.value)
}
onMounted(() => {
@@ -1285,14 +1238,11 @@ onMounted(() => {
path,
() => {
selected.value = []
keyword.value = ''
sub.value = false
sizeCache.value.clear()
sizeLoading.value.clear()
nextTick(() => {
refresh()
})
window.$bus.emit('file:push-history', path.value)
},
{ immediate: true }
)
+14 -42
View File
@@ -8,16 +8,18 @@ import copy2clipboard from '@vavt/copy2clipboard'
const { $gettext } = useGettext()
const fileStore = useFileStore()
const path = defineModel<string>('path', { type: String, required: true }) // 当前路径
const keyword = defineModel<string>('keyword', { type: String, default: '' }) // 搜索关键词
const sub = defineModel<boolean>('sub', { type: Boolean, default: false }) // 搜索是否包括子目录
const props = defineProps<{
tabId: string
}>()
const tab = computed(() => fileStore.tabs.find((t) => t.id === props.tabId)!)
const path = computed(() => tab.value.path)
const isInput = ref(false)
const pathInput = ref<InputInst | null>(null)
const input = ref('www')
const history: string[] = []
let current = -1
const handleInput = () => {
isInput.value = true
nextTick(() => {
@@ -43,8 +45,7 @@ const handleBlur = () => {
}
isInput.value = false
path.value = '/' + input.value
handlePushHistory(path.value)
fileStore.updateTabPath(props.tabId, '/' + input.value)
}
const handleRefresh = () => {
@@ -62,19 +63,11 @@ const handleUp = () => {
}
const handleBack = () => {
if (current > 0) {
current--
path.value = history[current] ?? '/'
input.value = path.value.slice(1)
}
fileStore.historyBack(props.tabId)
}
const handleForward = () => {
if (current < history.length - 1) {
current++
path.value = history[current] ?? '/'
input.value = path.value.slice(1)
}
fileStore.historyForward(props.tabId)
}
const splitPath = (str: string, delimiter: string) => {
@@ -88,20 +81,7 @@ const setPath = (index: number) => {
const newPath = splitPath(path.value, '/')
.slice(0, index + 1)
.join('/')
path.value = '/' + newPath
input.value = newPath
handlePushHistory(path.value)
}
const handlePushHistory = (path: string) => {
// 防止在前进后退时重复添加
if (current != history.length - 1) {
return
}
history.splice(current + 1)
history.push(path)
current = history.length - 1
fileStore.updateTabPath(props.tabId, '/' + newPath)
}
const handleSearch = () => {
@@ -115,14 +95,6 @@ watch(
},
{ immediate: true }
)
onMounted(() => {
window.$bus.on('file:push-history', handlePushHistory)
})
onUnmounted(() => {
window.$bus.off('file:push-history')
})
</script>
<template>
@@ -181,9 +153,9 @@ onUnmounted(() => {
/>
</n-input-group>
<n-input-group w-400>
<n-input v-model:value="keyword" :placeholder="$gettext('Enter search content')">
<n-input v-model:value="tab.keyword" :placeholder="$gettext('Enter search content')">
<template #suffix>
<n-checkbox v-model:checked="sub">
<n-checkbox v-model:checked="tab.sub">
{{ $gettext('Include subdirectories') }}
</n-checkbox>
</template>
+28 -91
View File
@@ -3,20 +3,26 @@ import file from '@/api/panel/file'
import PtyTerminalModal from '@/components/common/PtyTerminalModal.vue'
import { useFileStore } from '@/store'
import { checkName, lastDirectory } from '@/utils/file'
import type { Marked } from '@/views/file/types'
import { usePaste } from '@/views/file/composables/usePaste'
import { useGettext } from 'vue3-gettext'
const { $gettext } = useGettext()
const fileStore = useFileStore()
const { handlePaste: doPaste } = usePaste()
const props = defineProps<{
tabId: string
}>()
const path = defineModel<string>('path', { type: String, required: true })
const selected = defineModel<string[]>('selected', { type: Array, default: () => [] })
const marked = defineModel<Marked[]>('marked', { type: Array, default: () => [] })
const markedType = defineModel<string>('markedType', { type: String, required: true })
const compress = defineModel<boolean>('compress', { type: Boolean, required: true })
const permission = defineModel<boolean>('permission', { type: Boolean, required: true })
const upload = defineModel<boolean>('upload', { type: Boolean, required: true })
const tab = computed(() => fileStore.tabs.find((t) => t.id === props.tabId)!)
const path = computed(() => tab.value.path)
const marked = computed(() => fileStore.clipboard.marked)
// 终端弹窗
const terminalModal = ref(false)
@@ -58,12 +64,14 @@ const handleCopy = () => {
window.$message.error($gettext('Please select files/folders to copy'))
return
}
markedType.value = 'copy'
marked.value = selected.value.map((path) => ({
name: lastDirectory(path),
source: path,
force: false
}))
fileStore.setClipboard(
selected.value.map((p) => ({
name: lastDirectory(p),
source: p,
force: false
})),
'copy'
)
selected.value = []
window.$message.success(
$gettext('Marked successfully, please navigate to the destination path to paste')
@@ -75,12 +83,14 @@ const handleMove = () => {
window.$message.error($gettext('Please select files/folders to move'))
return
}
markedType.value = 'move'
marked.value = selected.value.map((path) => ({
name: lastDirectory(path),
source: path,
force: false
}))
fileStore.setClipboard(
selected.value.map((p) => ({
name: lastDirectory(p),
source: p,
force: false
})),
'move'
)
selected.value = []
window.$message.success(
$gettext('Marked successfully, please navigate to the destination path to paste')
@@ -88,84 +98,11 @@ const handleMove = () => {
}
const handleCancel = () => {
marked.value = []
fileStore.clearClipboard()
}
const handlePaste = () => {
if (!marked.value.length) {
window.$message.error($gettext('Please mark the files/folders to copy or move first'))
return
}
// 查重
let flag = false
const paths = marked.value.map((item) => {
return {
name: item.name,
source: item.source,
target: path.value + '/' + item.name,
force: false
}
})
const sources = paths.map((item: any) => item.target)
useRequest(file.exist(sources)).onSuccess(({ data }) => {
for (let i = 0; i < data.length; i++) {
if (data[i]) {
flag = true
const pathItem = paths[i]
if (pathItem) pathItem.force = true
}
}
if (flag) {
window.$dialog.warning({
title: $gettext('Warning'),
content: $gettext(
'There are items with the same name %{ items } Do you want to overwrite?',
{
items: `${paths
.filter((item) => item.force)
.map((item) => item.name)
.join(', ')}`
}
),
positiveText: $gettext('Overwrite'),
negativeText: $gettext('Cancel'),
onPositiveClick: async () => {
if (markedType.value == 'copy') {
useRequest(file.copy(paths)).onSuccess(() => {
marked.value = []
window.$bus.emit('file:refresh')
window.$message.success($gettext('Copied successfully'))
})
} else {
useRequest(file.move(paths)).onSuccess(() => {
marked.value = []
window.$bus.emit('file:refresh')
window.$message.success($gettext('Moved successfully'))
})
}
},
onNegativeClick: () => {
marked.value = []
window.$message.info($gettext('Canceled'))
}
})
} else {
if (markedType.value == 'copy') {
useRequest(file.copy(paths)).onSuccess(() => {
marked.value = []
window.$bus.emit('file:refresh')
window.$message.success($gettext('Copied successfully'))
})
} else {
useRequest(file.move(paths)).onSuccess(() => {
marked.value = []
window.$bus.emit('file:refresh')
window.$message.success($gettext('Moved successfully'))
})
}
}
})
doPaste(path.value)
}
const bulkDelete = async () => {
@@ -0,0 +1,72 @@
import file from '@/api/panel/file'
import { useFileStore } from '@/store'
import { useGettext } from 'vue3-gettext'
export function usePaste() {
const { $gettext } = useGettext()
const fileStore = useFileStore()
const handlePaste = (targetPath: string) => {
const { marked, markedType } = fileStore.clipboard
if (!marked.length) {
window.$message.error($gettext('Please mark the files/folders to copy or move first'))
return
}
const paths = marked.map((item) => ({
name: item.name,
source: item.source,
target: targetPath + '/' + item.name,
force: false
}))
const targets = paths.map((item) => item.target)
useRequest(file.exist(targets)).onSuccess(({ data }) => {
let hasConflict = false
for (let i = 0; i < data.length; i++) {
if (data[i]) {
hasConflict = true
const pathItem = paths[i]
if (pathItem) pathItem.force = true
}
}
const executePaste = () => {
const request = markedType === 'copy' ? file.copy(paths) : file.move(paths)
const successMsg =
markedType === 'copy' ? $gettext('Copied successfully') : $gettext('Moved successfully')
useRequest(request).onSuccess(() => {
fileStore.clearClipboard()
window.$bus.emit('file:refresh')
window.$message.success(successMsg)
})
}
if (hasConflict) {
window.$dialog.warning({
title: $gettext('Warning'),
content: $gettext(
'There are items with the same name %{ items } Do you want to overwrite?',
{
items: paths
.filter((item) => item.force)
.map((item) => item.name)
.join(', ')
}
),
positiveText: $gettext('Overwrite'),
negativeText: $gettext('Cancel'),
onPositiveClick: executePaste,
onNegativeClick: () => {
fileStore.clearClipboard()
window.$message.info($gettext('Canceled'))
}
})
} else {
executePaste()
}
})
}
return { handlePaste }
}
+1 -1
View File
@@ -104,7 +104,7 @@ const columns: any = [
class: 'cursor-pointer hover:opacity-60',
type: 'info',
onClick: () => {
fileStore.path = row.root_dir
fileStore.activeTab && fileStore.updateTabPath(fileStore.activeTabId, row.root_dir)
router.push({ name: 'file-index' })
}
},
+1 -1
View File
@@ -136,7 +136,7 @@ const columns: any = [
class: 'cursor-pointer hover:opacity-60',
type: 'info',
onClick: () => {
fileStore.path = row.path
fileStore.activeTab && fileStore.updateTabPath(fileStore.activeTabId, row.path)
router.push({ name: 'file-index' })
}
},