fix: Fix bug in file management with multiple tabs

This commit is contained in:
lan-yonghui 2025-08-28 10:59:25 +08:00
parent 5a34e2d380
commit 146350a9e2
14 changed files with 718 additions and 600 deletions

View file

@ -219,30 +219,30 @@ defineExpose({
closeRightClick,
});
onMounted(() => {
let heightDiff = 320;
let tabHeight = 0;
if (props.heightDiff) {
heightDiff = props.heightDiff;
}
if (globalStore.openMenuTabs) {
tabHeight = 48;
}
if (props.height) {
tableHeight.value = props.height - tabHeight;
} else {
tableHeight.value = window.innerHeight - heightDiff - tabHeight;
}
function calcHeight() {
let heightDiff = props.heightDiff ?? 320;
let tabHeight = globalStore.openMenuTabs ? 48 : 0;
window.onresize = () => {
return (() => {
if (props.height) {
tableHeight.value = props.height - tabHeight;
} else {
tableHeight.value = window.innerHeight - heightDiff - tabHeight;
}
})();
};
}
onMounted(() => {
calcHeight();
window.addEventListener('resize', calcHeight);
watch(
() => props.height,
() => {
calcHeight();
},
);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', calcHeight);
});
</script>

View file

@ -1510,6 +1510,7 @@ const message = {
cancelUpload: 'Cancel Upload',
cancelUploadHelper: 'Whether to cancel the upload, after cancellation the upload list will be cleared.',
keepOneTab: 'Keep at least one tab',
notCanTab: 'Cannot add more tabs',
},
ssh: {
autoStart: 'Auto start',

View file

@ -1454,6 +1454,7 @@ const message = {
cancelUpload: 'アップロードをキャンセル',
cancelUploadHelper: 'アップロードをキャンセルするかどうかキャンセル後アップロードリストはクリアされます',
keepOneTab: '少なくとも1つのタブを保持してください',
notCanTab: 'これ以上タブを追加できません',
},
ssh: {
autoStart: 'オートスタート',

View file

@ -1439,6 +1439,7 @@ const message = {
cancelUpload: '업로드 취소',
cancelUploadHelper: '업로드를 취소할지 여부, 취소 업로드 목록이 비워집니다.',
keepOneTab: '최소한 하나의 탭을 유지하세요',
notCanTab: ' 이상 탭을 추가할 없습니다',
},
ssh: {
autoStart: '자동 시작',

View file

@ -1497,6 +1497,7 @@ const message = {
cancelUploadHelper:
'Adakah hendak membatalkan muat naik, selepas pembatalan senarai muat naik akan dikosongkan.',
keepOneTab: 'Pastikan sekurang-kurangnya satu tab dikekalkan',
notCanTab: 'Tidak dapat menambah tab lagi',
},
ssh: {
autoStart: 'Mula automatik',

View file

@ -1484,6 +1484,7 @@ const message = {
cancelUpload: 'Cancelar Upload',
cancelUploadHelper: 'Deseja cancelar o upload, após o cancelamento, a lista de upload será limpa.',
keepOneTab: 'Mantenha pelo menos uma aba',
notCanTab: 'Não é possível adicionar mais abas',
},
ssh: {
autoStart: 'Início automático',

View file

@ -1485,6 +1485,7 @@ const message = {
cancelUpload: 'Отменить загрузку',
cancelUploadHelper: 'Отменить загрузку или нет, после отмены список загрузок будет очищен.',
keepOneTab: 'Необходимо оставить как минимум одну вкладку',
notCanTab: 'Невозможно добавить больше вкладок',
},
ssh: {
autoStart: 'Автозапуск',

View file

@ -1527,6 +1527,7 @@ const message = {
cancelUpload: 'Yüklemeyi İptal Et',
cancelUploadHelper: 'Yüklemeyi iptal etmek ister misiniz, iptal sonrası yükleme listesi temizlenecektir.',
keepOneTab: 'En az bir sekme ık kalmalıdır',
notCanTab: 'Daha fazla sekme eklenemez',
},
ssh: {
autoStart: 'Otomatik başlat',

View file

@ -1439,6 +1439,7 @@ const message = {
cancelUpload: '取消上傳',
cancelUploadHelper: '是否取消上傳取消後將清空上傳列表',
keepOneTab: '至少保留一個標籤頁',
notCanTab: '無法新增更多標籤頁',
},
ssh: {
autoStart: '開機自啟',

View file

@ -1434,6 +1434,7 @@ const message = {
cancelUpload: '取消上传',
cancelUploadHelper: '是否取消上传取消后将清空上传列表',
keepOneTab: '至少保留一个标签页',
notCanTab: '不可增加更多的标签页',
},
ssh: {
autoStart: '开机自启',

View file

@ -155,6 +155,7 @@ html.dark {
.el-tabs--card > .el-tabs__header .el-tabs__nav {
border: 1px solid var(--panel-main-bg-color-8);
border-bottom: none;
}
.el-tabs--card > .el-tabs__header .el-tabs__item.is-active {

View file

@ -257,6 +257,10 @@ html {
}
}
.el-tabs--card > .el-tabs__header .el-tabs__item.is-active {
border-bottom-color: var(--panel-color-primary) !important;
}
.logo {
color: var(--el-color-primary);
}

View file

@ -53,3 +53,42 @@ export function useSearchableForSelect(paths) {
searchableInputBlur,
};
}
export function useMultipleSearchable(paths) {
const searchableStatus = ref(false);
const searchablePath = ref('');
const searchableInputRefs = ref<Record<string, HTMLInputElement | null>>({});
const setSearchableInputRef = (id: string, el: HTMLInputElement | null) => {
if (el) {
searchableInputRefs.value[id] = el;
} else {
delete searchableInputRefs.value[id];
}
};
watch(searchableStatus, (val) => {
if (val) {
searchablePath.value = paths.value.at(-1)?.url || '';
nextTick(() => {
const keys = Object.keys(searchableInputRefs.value);
if (keys.length > 0) {
const lastKey = keys[keys.length - 1];
searchableInputRefs.value[lastKey]?.focus();
}
});
}
});
const searchableInputBlur = () => {
searchableStatus.value = false;
};
return {
searchableStatus,
searchablePath,
searchableInputRefs,
setSearchableInputRef,
searchableInputBlur,
};
}

View file

@ -1,5 +1,12 @@
<template>
<el-tabs type="card" v-model="editableTabsKey" @tab-change="changeTab" @tab-remove="removeTab">
<div>
<el-tabs
type="card"
class="file-tabs"
v-model="editableTabsKey"
@tab-change="changeTab"
@tab-remove="removeTab"
>
<el-tab-pane
closable
v-for="item in editableTabs"
@ -47,7 +54,11 @@
<el-icon :size="20"><HomeFilled /></el-icon>
</el-link>
</span>
<span v-for="(path, index) in paths" :key="path.url" class="inline-flex items-center">
<span
v-for="(path, index) in paths"
:key="path.url"
class="inline-flex items-center"
>
<span class="mr-2 arrow">></span>
<template v-if="index === 0 && hidePaths.length > 0">
<el-dropdown>
@ -121,7 +132,7 @@
</div>
</div>
<el-input
ref="searchableInputRef"
:ref="(el) => setSearchableInputRef(item.id, el)"
v-show="searchableStatus"
v-model="searchablePath"
@blur="searchableInputBlur"
@ -140,7 +151,11 @@
<el-icon :size="20"><HomeFilled /></el-icon>
</el-link>
</span>
<span v-for="(path, index) in paths" :key="path.url" class="inline-flex items-center">
<span
v-for="(path, index) in paths"
:key="path.url"
class="inline-flex items-center"
>
<span class="mr-2 arrow">></span>
<template v-if="index === 0 && hidePaths.length > 0">
<el-dropdown>
@ -313,11 +328,17 @@
<template #dropdown>
<el-dropdown-menu>
<template v-for="(mount, index) in hostMount" :key="mount.path">
<el-dropdown-item v-if="index == 0" @click.stop="jump(mount.path)">
<el-dropdown-item
v-if="index == 0"
@click.stop="jump(mount.path)"
>
{{ mount.path }} ({{ $t('file.root') }})
{{ formatFileSize(mount.free) }}
</el-dropdown-item>
<el-dropdown-item v-if="index != 0" @click.stop="jump(mount.path)">
<el-dropdown-item
v-if="index != 0"
@click.stop="jump(mount.path)"
>
{{ mount.path }} ({{ $t('home.mount') }})
{{ formatFileSize(mount.free) }}
</el-dropdown-item>
@ -437,7 +458,7 @@
@sort-change="changeSort"
@cell-mouse-enter="showFavorite"
@cell-mouse-leave="hideFavorite"
:heightDiff="340"
:heightDiff="heightDiff"
:right-buttons="buttons"
>
<el-table-column type="selection" width="30" />
@ -524,7 +545,12 @@
</el-link>
</template>
</el-table-column>
<el-table-column :label="$t('file.size')" prop="size" min-width="100" :sortable="'custom'">
<el-table-column
:label="$t('file.size')"
prop="size"
min-width="100"
:sortable="'custom'"
>
<template #default="{ row }">
<el-button
type="primary"
@ -585,11 +611,13 @@
</LayoutContent>
</div>
</el-tab-pane>
<el-tab-pane :closable="false" v-if="editableTabs.length < 6">
<el-tab-pane :closable="false" :disabled="editableTabs.length > 6">
<template #label>
<el-icon @click="addTab()"><Plus /></el-icon>
</template>
</el-tab-pane>
</el-tabs>
<CreateFile ref="createRef" @close="search" />
<ChangeRole ref="roleRef" @close="search" />
<Compress ref="compressRef" @close="search" />
@ -610,7 +638,7 @@
<VscodeOpenDialog ref="dialogVscodeOpenRef" />
<Preview ref="previewRef" />
<TerminalDialog ref="dialogTerminalRef" />
</el-tabs>
</div>
</template>
<script setup lang="ts">
@ -632,7 +660,7 @@ import { File } from '@/api/interface/file';
import { Languages, Mimetypes } from '@/global/mimetype';
import { useRouter } from 'vue-router';
import { MsgSuccess, MsgWarning } from '@/utils/message';
import { useSearchable } from './hooks/searchable';
import { useMultipleSearchable } from './hooks/searchable';
import { ResultData } from '@/api/interface';
import { GlobalStore } from '@/store';
import { Download as ElDownload, Upload as ElUpload, View, Hide } from '@element-plus/icons-vue';
@ -673,6 +701,7 @@ interface FilePaths {
const router = useRouter();
const data = ref();
const tableRefs = ref<Record<string, any>>({});
const heightDiff = ref(365);
const setTableRef = (key: string, el: any) => {
if (el) {
@ -765,7 +794,7 @@ const setPathRef = (key: string, el: any) => {
};
const getCurrentPath = () => pathRefs.value[editableTabsKey.value];
const { searchableStatus, searchablePath, searchableInputRef, searchableInputBlur } = useSearchable(paths);
const { searchableStatus, searchablePath, setSearchableInputRef, searchableInputBlur } = useMultipleSearchable(paths);
const paginationConfig = reactive({
cacheSizeKey: 'file-page-size',
@ -929,7 +958,7 @@ const btnResizeHandler = debounce(() => {
const observeResize = () => {
const el = getCurrentPath() as any;
if (!el) return;
resizeObserver = new ResizeObserver(() => {
let resizeObserver = new ResizeObserver(() => {
resizeHandler();
});
@ -942,6 +971,18 @@ const observeResize = () => {
resizeObserver.observe(ele);
};
function watchTitleHeight() {
const el = document.querySelector<HTMLElement>('.content-container__title');
if (el) {
let titleHeight = el.offsetHeight < 40 ? 40 : 80;
heightDiff.value = 325 + titleHeight;
}
}
watchTitleHeight();
window.addEventListener('resize', watchTitleHeight);
const resetPaths = () => {
paths.value = [...hidePaths.value, ...paths.value];
hidePaths.value = [];
@ -1684,61 +1725,75 @@ function updateTab(newPath?: string) {
}
const addTab = () => {
let tabIndex = editableTabs.value.length;
if (tabIndex >= 6) {
if (editableTabs.value.length >= 6) {
MsgWarning(i18n.global.t('file.notCanTab'));
return;
}
const usedIds = editableTabs.value.map((t) => Number(t.id));
let newId = null;
for (let i = 1; i <= 6; i++) {
if (!usedIds.includes(i)) {
newId = i;
break;
}
}
if (newId === null) {
MsgWarning(i18n.global.t('file.notCanTab'));
return;
}
const newTabId = `${++tabIndex}`;
editableTabs.value.push({
id: newTabId,
id: String(newId),
name: 'opt',
path: '/opt',
});
editableTabsKey.value = newTabId;
changeTab(newTabId);
editableTabsKey.value = String(newId);
changeTab(String(newId));
};
const changeTab = (targetPath: TabPaneName) => {
if (targetPath === 99) {
return;
}
editableTabsKey.value = targetPath.toString();
const current = editableTabs.value.find((tab) => tab.id === editableTabsKey.value);
editableTabsName.value = current ? current.name : '';
editableTabsValue.value = current ? current.path : '';
req.path = editableTabsValue.value;
paths.value = [];
const segments = editableTabsValue.value.split('/').filter(Boolean);
let url = '';
segments.forEach((segment) => {
url += '/' + segment;
paths.value.push({
url: editableTabsValue.value,
name: editableTabsName.value,
url,
name: segment,
});
});
search();
};
const removeTab = (targetPath: TabPaneName) => {
let tabIndex = editableTabs.value.length;
if (tabIndex === 1) {
const removeTab = (targetId: TabPaneName) => {
const tabs = editableTabs.value;
if (tabs.length <= 1) {
MsgWarning(i18n.global.t('file.keepOneTab'));
return;
}
editableTabsKey.value = targetPath.toString();
const tabs = editableTabs.value;
let activeKey = editableTabsKey.value;
if (activeKey === targetPath) {
tabs.forEach((tab, index) => {
if (tab.name === targetPath) {
const nextTab = tabs[index + 1] || tabs[index - 1];
if (nextTab) {
activeKey = nextTab.id;
const target = String(targetId);
const current = String(editableTabsKey.value);
const idx = tabs.findIndex((t) => String(t.id) === target);
if (idx === -1) return;
let nextActive = current;
if (current === target) {
nextActive = tabs[idx + 1]?.id ?? tabs[idx - 1]?.id ?? current;
}
}
});
}
editableTabsKey.value = activeKey;
editableTabs.value = tabs.filter((tab) => tab.id !== activeKey);
changeTab((Number(activeKey) - 1).toString());
editableTabs.value = tabs.filter((t) => String(t.id) !== target);
editableTabsKey.value = String(nextActive);
changeTab(String(nextActive));
};
onBeforeUnmount(() => {
if (resizeObserver) resizeObserver.disconnect();
window.removeEventListener('resize', watchTitleHeight);
});
</script>
@ -1818,4 +1873,14 @@ onBeforeUnmount(() => {
.table-input {
--el-input-inner-height: 22px !important;
}
:deep(.el-tabs__nav .el-tabs__item:last-child) {
border-bottom: 1px solid var(--el-border-color-light) !important;
}
:deep(.file-tabs .el-tabs--card > .el-tabs__header .el-tabs__item.is-active) {
border-bottom-width: 1px !important;
}
:deep(.file-tabs .el-tabs--card .el-tabs__header .el-tabs__nav) {
border-bottom: none !important;
}
</style>