mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-10 17:42:50 +08:00
366 lines
9.3 KiB
Vue
366 lines
9.3 KiB
Vue
<script lang="ts" setup>
|
|
import FileList from './filebrowser/FileList.vue'
|
|
import FileToolbar from './filebrowser/FileToolbar.vue'
|
|
import FileNavigator from './filebrowser/FileNavigator.vue'
|
|
import type { EndPoints, FileItem, StorageConf } from '@/api/types'
|
|
import { storageIconDict } from '@/api/constants'
|
|
import type { AxiosInstance } from 'axios'
|
|
|
|
// LocalStorage keys
|
|
const SORT_KEY = 'fileBrowser.sort'
|
|
const SHOW_TREE_KEY = 'fileBrowser.showDirTree'
|
|
const NAV_WIDTH_KEY = 'fileBrowser.navigatorWidth'
|
|
|
|
// 输入参数
|
|
const props = defineProps({
|
|
storages: Array as PropType<StorageConf[]>,
|
|
tree: Boolean,
|
|
endpoints: Object as PropType<EndPoints>,
|
|
axios: {
|
|
type: Object as PropType<AxiosInstance>,
|
|
required: true,
|
|
},
|
|
axiosconfig: Object,
|
|
item: {
|
|
type: Object as PropType<FileItem>,
|
|
required: true,
|
|
},
|
|
itemstack: {
|
|
type: Array as PropType<FileItem[]>,
|
|
default: () => [],
|
|
},
|
|
})
|
|
|
|
// 对外事件
|
|
const emit = defineEmits(['pathchanged'])
|
|
|
|
const fileIcons = {
|
|
// 压缩包
|
|
zip: 'mdi-folder-zip-outline',
|
|
rar: 'mdi-folder-zip-outline',
|
|
bak: 'mdi-folder-zip-outline',
|
|
tar: 'mdi-folder-zip-outline',
|
|
gz: 'mdi-folder-zip-outline',
|
|
bz2: 'mdi-folder-zip-outline',
|
|
// 开发
|
|
htm: 'mdi-language-html5',
|
|
html: 'mdi-language-html5',
|
|
vue: 'mdi-vuejs',
|
|
js: 'mdi-nodejs',
|
|
ts: 'mdi-language-typescript',
|
|
json: 'mdi-file-document-outline',
|
|
css: 'mdi-language-css3',
|
|
scss: 'mdi-language-css3',
|
|
less: 'mdi-language-css3',
|
|
php: 'mdi-language-php',
|
|
py: 'mdi-language-python',
|
|
java: 'mdi-language-java',
|
|
go: 'mdi-language-go',
|
|
c: 'mdi-language-c',
|
|
cpp: 'mdi-language-cpp',
|
|
h: 'mdi-language-c',
|
|
cs: 'mdi-language-csharp',
|
|
sql: 'mdi-database',
|
|
sh: 'mdi-language-bash',
|
|
bat: 'mdi-language-bash',
|
|
ps1: 'mdi-language-powershell',
|
|
// markdown
|
|
md: 'mdi-language-markdown-outline',
|
|
markdown: 'mdi-language-markdown-outline',
|
|
// 图片
|
|
png: 'mdi-file-png-box',
|
|
jpg: 'mdi-file-jpg-box',
|
|
jpeg: 'mdi-file-jpg-box',
|
|
gif: 'mdi-file-gif-box',
|
|
bmp: 'mdi-file-image-box',
|
|
webp: 'mdi-file-image-box',
|
|
ico: 'mdi-file-image-box',
|
|
svg: 'mdi-file-image-box',
|
|
// 视频
|
|
mp4: 'mdi-filmstrip',
|
|
mkv: 'mdi-filmstrip',
|
|
avi: 'mdi-filmstrip',
|
|
wmv: 'mdi-filmstrip',
|
|
mov: 'mdi-filmstrip',
|
|
flv: 'mdi-filmstrip',
|
|
rmvb: 'mdi-filmstrip',
|
|
// 文档
|
|
txt: 'mdi-file-document-outline',
|
|
env: 'mdi-file-cog-outline',
|
|
yml: 'mdi-file-cog-outline',
|
|
yaml: 'mdi-file-cog-outline',
|
|
conf: 'mdi-file-cog-outline',
|
|
log: 'mdi-file-document-outline',
|
|
csv: 'mdi-file-delimited',
|
|
// office
|
|
xls: 'mdi-file-excel',
|
|
xlsx: 'mdi-file-excel',
|
|
doc: 'mdi-file-word',
|
|
docx: 'mdi-file-word',
|
|
ppt: 'mdi-file-powerpoint',
|
|
pptx: 'mdi-file-powerpoint',
|
|
pdf: 'mdi-file-pdf',
|
|
// 音频
|
|
mp2: 'mdi-music',
|
|
mp3: 'mdi-music',
|
|
m4a: 'mdi-music',
|
|
wma: 'mdi-music',
|
|
aac: 'mdi-music',
|
|
ogg: 'mdi-music',
|
|
flac: 'mdi-music',
|
|
wav: 'mdi-music',
|
|
// 字体
|
|
ttf: 'mdi-format-font',
|
|
otf: 'mdi-format-font',
|
|
woff: 'mdi-format-font',
|
|
woff2: 'mdi-format-font',
|
|
eot: 'mdi-format-font',
|
|
// 字幕
|
|
srt: 'mdi-subtitles-outline',
|
|
ass: 'mdi-subtitles-outline',
|
|
sub: 'mdi-subtitles-outline',
|
|
// 其他
|
|
other: 'mdi-file-outline',
|
|
}
|
|
|
|
// 加载次数
|
|
const loading = ref(0)
|
|
|
|
// 刷新
|
|
const refreshPending = ref(false)
|
|
// 排序 - 从localStorage恢复
|
|
const sort = ref(localStorage.getItem(SORT_KEY) || 'name')
|
|
|
|
// 是否显示目录树 - 从localStorage恢复
|
|
const showDirTree = ref(localStorage.getItem(SHOW_TREE_KEY) === 'true')
|
|
|
|
// 拖动分隔条相关 - 从localStorage恢复宽度
|
|
const navigatorWidth = ref(parseInt(localStorage.getItem(NAV_WIDTH_KEY) || '280'))
|
|
const isDragging = ref(false)
|
|
const dragStartX = ref(0)
|
|
const dragStartWidth = ref(0)
|
|
|
|
watch(sort, (val) => {
|
|
localStorage.setItem(SORT_KEY, val)
|
|
})
|
|
|
|
watch(showDirTree, (val) => {
|
|
localStorage.setItem(SHOW_TREE_KEY, String(val))
|
|
})
|
|
|
|
watch(navigatorWidth, (val) => {
|
|
localStorage.setItem(NAV_WIDTH_KEY, String(val))
|
|
})
|
|
|
|
// 计算属性
|
|
const storagesArray = computed(() => {
|
|
return props.storages?.map(item => ({
|
|
title: item.name,
|
|
value: item.type,
|
|
icon: storageIconDict[item.type] ?? 'mdi-server-network-outline',
|
|
}))
|
|
})
|
|
|
|
|
|
// 方法
|
|
function loadingChanged(isLoading: number) {
|
|
if (isLoading) loading.value++
|
|
else if (loading.value > 0) loading.value--
|
|
}
|
|
|
|
// 存储切换
|
|
async function storageChanged(storage: string) {
|
|
emit('pathchanged', { storage: storage, path: '/', fileid: 'root' })
|
|
}
|
|
|
|
// 路径变化
|
|
function pathChanged(item: FileItem) {
|
|
emit('pathchanged', item)
|
|
}
|
|
|
|
// 排序变化
|
|
function sortChanged(s: string) {
|
|
sort.value = s
|
|
refreshPending.value = true
|
|
}
|
|
|
|
// 切换目录树
|
|
function switchDirTree(state: boolean) {
|
|
showDirTree.value = state
|
|
}
|
|
|
|
// 文件列表
|
|
const fileListItems = ref<FileItem[]>([])
|
|
|
|
// 文件列表数据更新
|
|
function fileListUpdated(items: FileItem[]) {
|
|
fileListItems.value = items
|
|
}
|
|
|
|
// 阻止选择事件
|
|
function preventSelect(event: Event) {
|
|
event.preventDefault()
|
|
return false
|
|
}
|
|
|
|
// 拖动分隔条相关方法
|
|
function startDrag(event: MouseEvent) {
|
|
event.preventDefault() // 阻止默认行为
|
|
event.stopPropagation() // 阻止事件冒泡
|
|
|
|
isDragging.value = true
|
|
dragStartX.value = event.clientX
|
|
dragStartWidth.value = navigatorWidth.value
|
|
|
|
document.addEventListener('mousemove', handleDrag, { passive: false })
|
|
document.addEventListener('mouseup', stopDrag, { passive: false })
|
|
document.addEventListener('selectstart', preventSelect) // 阻止选择开始
|
|
|
|
document.body.style.cursor = 'col-resize'
|
|
document.body.style.userSelect = 'none'
|
|
;(document.body.style as any).webkitUserSelect = 'none' // Safari兼容
|
|
;(document.body.style as any).mozUserSelect = 'none' // Firefox兼容
|
|
}
|
|
|
|
function handleDrag(event: MouseEvent) {
|
|
if (!isDragging.value) return
|
|
|
|
event.preventDefault() // 阻止默认行为
|
|
|
|
const deltaX = event.clientX - dragStartX.value
|
|
const newWidth = dragStartWidth.value + deltaX
|
|
|
|
// 设置最小和最大宽度限制
|
|
const minWidth = 200
|
|
const maxWidth = window.innerWidth * 0.6
|
|
|
|
navigatorWidth.value = Math.max(minWidth, Math.min(maxWidth, newWidth))
|
|
}
|
|
|
|
function stopDrag() {
|
|
isDragging.value = false
|
|
document.removeEventListener('mousemove', handleDrag)
|
|
document.removeEventListener('mouseup', stopDrag)
|
|
document.removeEventListener('selectstart', preventSelect)
|
|
|
|
document.body.style.cursor = ''
|
|
document.body.style.userSelect = ''
|
|
;(document.body.style as any).webkitUserSelect = ''
|
|
;(document.body.style as any).mozUserSelect = ''
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="mx-auto" :loading="loading > 0">
|
|
<div v-if="item">
|
|
<FileToolbar
|
|
:sort="sort"
|
|
:item="item"
|
|
:itemstack="itemstack"
|
|
:storages="storagesArray"
|
|
:endpoints="endpoints"
|
|
:axios="axios"
|
|
@storagechanged="storageChanged"
|
|
@pathchanged="pathChanged"
|
|
@foldercreated="refreshPending = true"
|
|
@sortchanged="sortChanged"
|
|
/>
|
|
<div class="flex">
|
|
<FileNavigator
|
|
v-if="showDirTree"
|
|
:storage="item.storage"
|
|
:currentPath="item.path"
|
|
:items="fileListItems"
|
|
:endpoints="endpoints"
|
|
:axios="axios"
|
|
:style="{ width: `${navigatorWidth}px`, minWidth: `${navigatorWidth}px` }"
|
|
@navigate="pathChanged"
|
|
/>
|
|
<!-- 拖动分隔条 -->
|
|
<div v-if="showDirTree" class="divider" :class="{ 'divider-dragging': isDragging }" @mousedown="startDrag">
|
|
<div class="divider-line"></div>
|
|
<VIcon class="divider-icon" size="small">mdi-drag-vertical</VIcon>
|
|
</div>
|
|
<FileList
|
|
:item="item"
|
|
:icons="fileIcons"
|
|
:endpoints="endpoints"
|
|
:axios="axios"
|
|
:refreshpending="refreshPending"
|
|
:sort="sort"
|
|
:showTree="showDirTree"
|
|
:style="{ flex: 1 }"
|
|
@pathchanged="pathChanged"
|
|
@loading="loadingChanged"
|
|
@refreshed="refreshPending = false"
|
|
@filedeleted="refreshPending = true"
|
|
@renamed="refreshPending = true"
|
|
@items-updated="fileListUpdated"
|
|
@switch-tree="switchDirTree"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.divider {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background-color: transparent;
|
|
cursor: col-resize;
|
|
inline-size: 4px;
|
|
transition: background-color 0.2s ease;
|
|
user-select: none;
|
|
}
|
|
|
|
.divider:hover {
|
|
background-color: rgba(var(--v-theme-on-surface), 0.08);
|
|
}
|
|
|
|
.divider-dragging {
|
|
background-color: rgba(var(--v-theme-primary), 0.12) !important;
|
|
}
|
|
|
|
.divider-line {
|
|
background-color: rgba(var(--v-theme-outline), 0.3);
|
|
block-size: 100%;
|
|
inline-size: 1px;
|
|
transition: background-color 0.2s ease;
|
|
user-select: none;
|
|
}
|
|
|
|
.divider-dragging .divider-line {
|
|
background-color: rgb(var(--v-theme-primary)) !important;
|
|
}
|
|
|
|
.divider:hover .divider-line {
|
|
background-color: rgba(var(--v-theme-primary), 0.8);
|
|
}
|
|
|
|
.divider-icon {
|
|
position: absolute;
|
|
z-index: 1;
|
|
padding: 2px;
|
|
border-radius: 2px;
|
|
background-color: rgba(var(--v-theme-surface), 0.9);
|
|
color: rgba(var(--v-theme-on-surface-variant), 0.6);
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.divider-dragging .divider-icon {
|
|
background-color: rgba(var(--v-theme-surface), 0.95);
|
|
color: rgb(var(--v-theme-primary));
|
|
opacity: 1;
|
|
}
|
|
|
|
.divider:hover .divider-icon {
|
|
color: rgba(var(--v-theme-primary), 0.9);
|
|
opacity: 1;
|
|
}
|
|
</style>
|