feat: UI优化

This commit is contained in:
耗子
2026-02-11 20:38:08 +08:00
parent 2dd6f0d9f0
commit c5d62a46ab
5 changed files with 148 additions and 199 deletions
+2
View File
@@ -53,6 +53,7 @@
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.5.0",
"remove": "^0.1.5",
"sortablejs": "^1.15.6",
"vue": "^3.5.22",
"vue-echarts": "^8.0.1",
"vue-router": "^5.0.0",
@@ -68,6 +69,7 @@
"@types/luxon": "^3.7.1",
"@types/node": "^24.0.0",
"@types/node-forge": "^1.3.14",
"@types/sortablejs": "^1.15.9",
"@unocss/eslint-config": "^66.5.4",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/eslint-config-prettier": "^10.2.0",
+16
View File
@@ -92,6 +92,9 @@ importers:
remove:
specifier: ^0.1.5
version: 0.1.5
sortablejs:
specifier: ^1.15.6
version: 1.15.6
vue:
specifier: ^3.5.22
version: 3.5.28(typescript@5.9.3)
@@ -132,6 +135,9 @@ importers:
'@types/node-forge':
specifier: ^1.3.14
version: 1.3.14
'@types/sortablejs':
specifier: ^1.15.9
version: 1.15.9
'@unocss/eslint-config':
specifier: ^66.5.4
version: 66.6.0(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3)
@@ -1091,6 +1097,9 @@ packages:
'@types/node@24.10.13':
resolution: {integrity: sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==}
'@types/sortablejs@1.15.9':
resolution: {integrity: sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
@@ -2984,6 +2993,9 @@ packages:
sortablejs@1.14.0:
resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==}
sortablejs@1.15.6:
resolution: {integrity: sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -4433,6 +4445,8 @@ snapshots:
dependencies:
undici-types: 7.16.0
'@types/sortablejs@1.15.9': {}
'@types/trusted-types@2.0.7':
optional: true
@@ -6599,6 +6613,8 @@ snapshots:
sortablejs@1.14.0: {}
sortablejs@1.15.6: {}
source-map-js@1.2.1: {}
source-map-support@0.5.21:
-7
View File
@@ -25,9 +25,6 @@ export interface FileState {
}
}
// 最大标签页数量
const MAX_TABS = 10
// 根据路径生成标签页显示名
const getLabelFromPath = (path: string): string => {
if (path === '/') return '/'
@@ -73,10 +70,6 @@ export const useFileStore = defineStore('file', {
actions: {
// 新建标签页
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)
+98 -180
View File
@@ -3,11 +3,9 @@ defineOptions({
name: 'file-index'
})
import { useThemeVars } from 'naive-ui'
import draggable from 'vuedraggable'
import Sortable from 'sortablejs'
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'
@@ -17,7 +15,6 @@ import UploadModal from '@/views/file/UploadModal.vue'
import type { FileInfo } from '@/views/file/types'
const fileStore = useFileStore()
const themeVars = useThemeVars()
const selected = ref<string[]>([])
// 权限编辑时的文件信息列表
@@ -31,12 +28,6 @@ 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,
@@ -45,7 +36,78 @@ watch(
}
)
// 处理拖拽进入
// n-tabs 事件
const handleTabSwitch = (tabId: string | number) => {
fileStore.switchTab(tabId as string)
}
const handleTabClose = (tabId: string | number) => {
fileStore.closeTab(tabId as string)
}
const handleTabAdd = () => {
fileStore.createTab()
}
// 中键关闭标签页
const handleTabMiddleClick = (tabId: string) => {
if (fileStore.tabs.length > 1) {
fileStore.closeTab(tabId)
}
}
// ==================== SortableJS 拖拽排序 ====================
const tabsRef = ref<InstanceType<(typeof import('naive-ui'))['NTabs']> | null>(null)
let sortableInstance: Sortable | null = null
const initSortable = () => {
nextTick(() => {
sortableInstance?.destroy()
sortableInstance = null
// n-tabs 内部结构: .n-tabs-wrapper 包含 scroll-padding + tab-wrapper + addable
const container = tabsRef.value?.$el?.querySelector('.n-tabs-wrapper')
if (!container) return
sortableInstance = Sortable.create(container as HTMLElement, {
animation: 200,
draggable: '.n-tabs-tab-wrapper',
filter: '.n-tabs-scroll-padding, .n-tabs-tab--addable',
preventOnFilter: false,
onMove: (evt) => {
// 禁止拖到 scroll-padding 和 addable 按钮的位置
const related = evt.related as HTMLElement
if (
related.classList.contains('n-tabs-scroll-padding') ||
related.querySelector('.n-tabs-tab--addable')
) {
return false
}
},
onEnd: (evt) => {
const { oldIndex, newIndex } = evt
if (oldIndex == null || newIndex == null || oldIndex === newIndex) return
// scroll-padding 占了 index 0,实际 tab 从 index 1 开始,需要偏移
const realOld = oldIndex - 1
const realNew = newIndex - 1
if (realOld < 0 || realNew < 0) return
const tabs = [...fileStore.tabs]
const [moved] = tabs.splice(realOld, 1)
if (moved) {
tabs.splice(realNew, 0, moved)
fileStore.reorderTabs(tabs)
}
}
})
})
}
onMounted(initSortable)
// 标签页增减时重新初始化 Sortable
watch(() => fileStore.tabs.length, initSortable)
onUnmounted(() => {
sortableInstance?.destroy()
})
// ==================== 文件拖拽上传 ====================
const handleDragEnter = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
@@ -54,7 +116,6 @@ const handleDragEnter = (e: DragEvent) => {
}
}
// 处理拖拽离开
const handleDragLeave = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
@@ -69,7 +130,6 @@ const handleDragLeave = (e: DragEvent) => {
}
}
// 处理拖拽悬停
const handleDragOver = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
@@ -119,7 +179,6 @@ const readDirectoryRecursively = async (
return files
}
// 处理拖拽放下
const handleDrop = async (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
@@ -161,25 +220,6 @@ 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>
@@ -193,40 +233,29 @@ const tabStyles = computed(() => ({
>
<n-flex vertical :size="20" class="flex-1 min-h-0">
<!-- 标签页栏 -->
<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)"
<n-tabs
ref="tabsRef"
type="card"
size="small"
:value="fileStore.activeTabId"
:closable="fileStore.tabs.length > 1"
addable
class="file-tabs"
@update:value="handleTabSwitch"
@close="handleTabClose"
@add="handleTabAdd"
>
<n-tab-pane v-for="tab in fileStore.tabs" :key="tab.id" :name="tab.id">
<template #tab>
<span
:title="tab.path"
@mousedown.middle.prevent="handleTabMiddleClick(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>
{{ tab.label }}
</span>
</template>
<template #footer>
<div class="file-tab-add" @click="fileStore.createTab()">
<i-mdi-plus :size="16" />
</div>
</template>
</draggable>
</div>
</n-tab-pane>
</n-tabs>
<!-- 每个标签页内容v-if 只渲染活跃的 -->
<template v-for="tab in fileStore.tabs" :key="tab.id">
@@ -299,128 +328,17 @@ const tabStyles = computed(() => ({
font-size: 18px;
}
// n-tabs 只用作导航栏,隐藏空的 pane 区域
.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 {
:deep(.n-tabs-pane-wrapper) {
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;
}
:deep(.n-tabs-tab) {
padding: 4px 12px !important;
}
&.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>
+32 -12
View File
@@ -100,18 +100,38 @@ watch(
<template>
<n-flex>
<n-button-group>
<n-button @click="handleBack">
<i-mdi-arrow-left :size="16" />
</n-button>
<n-button @click="handleForward">
<i-mdi-arrow-right :size="16" />
</n-button>
<n-button @click="handleUp">
<i-mdi-arrow-up :size="16" />
</n-button>
<n-button @click="handleRefresh">
<i-mdi-refresh :size="16" />
</n-button>
<n-tooltip>
<template #trigger>
<n-button @click="handleBack">
<i-mdi-arrow-left :size="16" />
</n-button>
</template>
{{ $gettext('Back') }}
</n-tooltip>
<n-tooltip>
<template #trigger>
<n-button @click="handleForward">
<i-mdi-arrow-right :size="16" />
</n-button>
</template>
{{ $gettext('Forward') }}
</n-tooltip>
<n-tooltip>
<template #trigger>
<n-button @click="handleUp">
<i-mdi-arrow-up :size="16" />
</n-button>
</template>
{{ $gettext('Up') }}
</n-tooltip>
<n-tooltip>
<template #trigger>
<n-button @click="handleRefresh">
<i-mdi-refresh :size="16" />
</n-button>
</template>
{{ $gettext('Refresh') }}
</n-tooltip>
<n-tooltip>
<template #trigger>
<n-button @click="toggleHidden" :type="fileStore.showHidden ? 'primary' : 'default'">