mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-09-07 15:14:36 +08:00
fix: Fix bug in file management with multiple tabs
This commit is contained in:
parent
5a34e2d380
commit
146350a9e2
14 changed files with 718 additions and 600 deletions
|
@ -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>
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -1454,6 +1454,7 @@ const message = {
|
|||
cancelUpload: 'アップロードをキャンセル',
|
||||
cancelUploadHelper: 'アップロードをキャンセルするかどうか、キャンセル後、アップロードリストはクリアされます。',
|
||||
keepOneTab: '少なくとも1つのタブを保持してください',
|
||||
notCanTab: 'これ以上タブを追加できません',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: 'オートスタート',
|
||||
|
|
|
@ -1439,6 +1439,7 @@ const message = {
|
|||
cancelUpload: '업로드 취소',
|
||||
cancelUploadHelper: '업로드를 취소할지 여부, 취소 후 업로드 목록이 비워집니다.',
|
||||
keepOneTab: '최소한 하나의 탭을 유지하세요',
|
||||
notCanTab: '더 이상 탭을 추가할 수 없습니다',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: '자동 시작',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -1485,6 +1485,7 @@ const message = {
|
|||
cancelUpload: 'Отменить загрузку',
|
||||
cancelUploadHelper: 'Отменить загрузку или нет, после отмены список загрузок будет очищен.',
|
||||
keepOneTab: 'Необходимо оставить как минимум одну вкладку',
|
||||
notCanTab: 'Невозможно добавить больше вкладок',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: 'Автозапуск',
|
||||
|
|
|
@ -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 açık kalmalıdır',
|
||||
notCanTab: 'Daha fazla sekme eklenemez',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: 'Otomatik başlat',
|
||||
|
|
|
@ -1439,6 +1439,7 @@ const message = {
|
|||
cancelUpload: '取消上傳',
|
||||
cancelUploadHelper: '是否取消上傳,取消後將清空上傳列表',
|
||||
keepOneTab: '至少保留一個標籤頁',
|
||||
notCanTab: '無法新增更多標籤頁',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: '開機自啟',
|
||||
|
|
|
@ -1434,6 +1434,7 @@ const message = {
|
|||
cancelUpload: '取消上传',
|
||||
cancelUploadHelper: '是否取消上传,取消后将清空上传列表',
|
||||
keepOneTab: '至少保留一个标签页',
|
||||
notCanTab: '不可增加更多的标签页',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: '开机自启',
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Reference in a new issue