mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-10 23:47:39 +08:00
feat: Multi directory calculation size (#8762)
* feat: Multi directory calculation size * feat: optimize code editor * feat: directory calculation size
This commit is contained in:
parent
58d9c068e4
commit
057dcaf2e5
19 changed files with 297 additions and 59 deletions
|
@ -628,6 +628,28 @@ func (b *BaseApi) Size(c *gin.Context) {
|
|||
helper.SuccessWithData(c, res)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Multi file size
|
||||
// @Accept json
|
||||
// @Param request body request.DirSizeReq true "request"
|
||||
// @Success 200 {array} response.DepthDirSizeRes
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/depth/size [post]
|
||||
// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"获取目录及其第一层子目录文件夹大小 [path]","formatEN":"Multi file size [path]"}
|
||||
func (b *BaseApi) DepthDirSize(c *gin.Context) {
|
||||
var req request.DirSizeReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
res, err := fileService.DepthDirSize(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, res)
|
||||
}
|
||||
|
||||
func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int, overwrite bool) error {
|
||||
defer func() {
|
||||
_ = os.RemoveAll(fileDir)
|
||||
|
|
|
@ -64,3 +64,8 @@ type UserGroupResponse struct {
|
|||
Users []UserInfo `json:"users"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
|
||||
type DepthDirSizeRes struct {
|
||||
Path string `json:"path"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/jinzhu/copier"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
|
@ -51,6 +52,7 @@ type IFileService interface {
|
|||
SaveContent(edit request.FileEdit) error
|
||||
FileDownload(d request.FileDownload) (string, error)
|
||||
DirSize(req request.DirSizeReq) (response.DirSizeRes, error)
|
||||
DepthDirSize(req request.DirSizeReq) ([]response.DepthDirSizeRes, error)
|
||||
ChangeName(req request.FileRename) error
|
||||
Wget(w request.FileWget) (string, error)
|
||||
MvFile(m request.FileMove) error
|
||||
|
@ -443,6 +445,22 @@ func (f *FileService) DirSize(req request.DirSizeReq) (response.DirSizeRes, erro
|
|||
return res, nil
|
||||
}
|
||||
|
||||
func (f *FileService) DepthDirSize(req request.DirSizeReq) ([]response.DepthDirSizeRes, error) {
|
||||
var (
|
||||
res []response.DepthDirSizeRes
|
||||
)
|
||||
if req.Path == "/proc" {
|
||||
return res, nil
|
||||
}
|
||||
fo := files.NewFileOp()
|
||||
dirSizes, err := fo.GetDepthDirSize(req.Path)
|
||||
_ = copier.Copy(&res, &dirSizes)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.FileLineContent, error) {
|
||||
logFilePath := ""
|
||||
switch req.Type {
|
||||
|
|
|
@ -34,6 +34,7 @@ func (f *FileRouter) InitRouter(Router *gin.RouterGroup) {
|
|||
fileRouter.GET("/download", baseApi.Download)
|
||||
fileRouter.POST("/chunkdownload", baseApi.DownloadChunkFiles)
|
||||
fileRouter.POST("/size", baseApi.Size)
|
||||
fileRouter.POST("/depth/size", baseApi.DepthDirSize)
|
||||
fileRouter.GET("/wget/process", baseApi.WgetProcess)
|
||||
fileRouter.GET("/wget/process/keys", baseApi.ProcessKeys)
|
||||
fileRouter.POST("/read", baseApi.ReadFileByLine)
|
||||
|
|
|
@ -512,6 +512,71 @@ func (f FileOp) GetDirSize(path string) (int64, error) {
|
|||
return size, nil
|
||||
}
|
||||
|
||||
type DirSize struct {
|
||||
Path string `json:"path"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
func (f FileOp) GetDepthDirSize(path string) ([]DirSize, error) {
|
||||
var result []DirSize
|
||||
sizeMap := make(map[string]int64)
|
||||
duCmd := exec.Command("du", "-k", "--max-depth=1", "--exclude=proc", path)
|
||||
output, err := duCmd.Output()
|
||||
if err == nil {
|
||||
parseDUOutput(output, sizeMap)
|
||||
} else {
|
||||
calculateDirSizeFallback(path, sizeMap)
|
||||
}
|
||||
|
||||
for dir, size := range sizeMap {
|
||||
result = append(result, DirSize{
|
||||
Path: dir,
|
||||
Size: size,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseDUOutput(output []byte, sizeMap map[string]int64) {
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) == 2 {
|
||||
if sizeKB, err := strconv.ParseInt(fields[0], 10, 64); err == nil {
|
||||
dir := fields[1]
|
||||
sizeMap[dir] = sizeKB * 1024
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func calculateDirSizeFallback(path string, sizeMap map[string]int64) {
|
||||
_ = filepath.Walk(path, func(p string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if !info.IsDir() {
|
||||
rel, err := filepath.Rel(path, p)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(rel, string(os.PathSeparator))
|
||||
var topLevel string
|
||||
if len(parts) == 0 || parts[0] == "." {
|
||||
topLevel = path
|
||||
} else {
|
||||
topLevel = filepath.Join(path, parts[0])
|
||||
}
|
||||
sizeMap[topLevel] += info.Size()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func getFormat(cType CompressType) archiver.CompressedArchive {
|
||||
format := archiver.CompressedArchive{}
|
||||
switch cType {
|
||||
|
|
|
@ -152,6 +152,11 @@ export namespace File {
|
|||
size: number;
|
||||
}
|
||||
|
||||
export interface DepthDirSizeRes {
|
||||
size: number;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface FilePath {
|
||||
path: string;
|
||||
}
|
||||
|
|
|
@ -98,6 +98,10 @@ export const computeDirSize = (params: File.DirSizeReq) => {
|
|||
return http.post<File.DirSizeRes>('files/size', params, TimeoutEnum.T_5M);
|
||||
};
|
||||
|
||||
export const computeDepthDirSize = (params: File.DirSizeReq) => {
|
||||
return http.post<File.DepthDirSizeRes[]>('files/depth/size', params, TimeoutEnum.T_5M);
|
||||
};
|
||||
|
||||
export const fileWgetKeys = () => {
|
||||
return http.get<File.FileKeys>('files/wget/process/keys');
|
||||
};
|
||||
|
|
|
@ -21,7 +21,12 @@
|
|||
</fu-table>
|
||||
</div>
|
||||
|
||||
<div class="complex-table__pagination" v-if="props.paginationConfig">
|
||||
<div
|
||||
class="complex-table__pagination flex items-center w-full"
|
||||
v-if="props.paginationConfig"
|
||||
:class="{ '!justify-between': slots.paginationLeft, '!justify-end': !slots.paginationLeft }"
|
||||
>
|
||||
<slot name="paginationLeft"></slot>
|
||||
<slot name="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="paginationConfig.currentPage"
|
||||
|
@ -100,21 +105,25 @@ defineExpose({
|
|||
|
||||
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;
|
||||
tableHeight.value = props.height - tabHeight;
|
||||
} else {
|
||||
tableHeight.value = window.innerHeight - heightDiff;
|
||||
tableHeight.value = window.innerHeight - heightDiff - tabHeight;
|
||||
}
|
||||
|
||||
window.onresize = () => {
|
||||
return (() => {
|
||||
if (props.height) {
|
||||
tableHeight.value = props.height;
|
||||
tableHeight.value = props.height - tabHeight;
|
||||
} else {
|
||||
tableHeight.value = window.innerHeight - heightDiff;
|
||||
tableHeight.value = window.innerHeight - heightDiff - tabHeight;
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
|
|
@ -80,6 +80,7 @@ const message = {
|
|||
fix: 'Fix',
|
||||
down: 'Stop',
|
||||
up: 'Start',
|
||||
sure: 'Confirm',
|
||||
},
|
||||
operate: {
|
||||
start: 'Start',
|
||||
|
@ -1342,6 +1343,7 @@ const message = {
|
|||
taskRunning: 'Running',
|
||||
},
|
||||
file: {
|
||||
currentDir: 'Directory',
|
||||
dir: 'Folder',
|
||||
fileName: 'File name',
|
||||
search: 'Find',
|
||||
|
|
|
@ -77,6 +77,7 @@ const message = {
|
|||
fix: '修正',
|
||||
down: '停止',
|
||||
up: '起動',
|
||||
sure: '確認',
|
||||
},
|
||||
operate: {
|
||||
start: '開始',
|
||||
|
@ -1278,6 +1279,7 @@ const message = {
|
|||
errLog: 'エラーログ',
|
||||
},
|
||||
file: {
|
||||
currentDir: '現在のディレクトリ',
|
||||
dir: 'フォルダ',
|
||||
upload: 'アップロード',
|
||||
uploadFile: '@:file.upload@.lower:file.file',
|
||||
|
|
|
@ -77,6 +77,7 @@ const message = {
|
|||
fix: '수정',
|
||||
down: '중지',
|
||||
up: '시작',
|
||||
sure: '확인',
|
||||
},
|
||||
operate: {
|
||||
start: '시작',
|
||||
|
@ -1265,6 +1266,7 @@ const message = {
|
|||
errLog: '에러 로그',
|
||||
},
|
||||
file: {
|
||||
currentDir: '현재 디렉터리',
|
||||
dir: '폴더',
|
||||
upload: '업로드',
|
||||
uploadFile: '@:file.upload @.lower:file.file',
|
||||
|
|
|
@ -77,6 +77,7 @@ const message = {
|
|||
fix: 'Betulkan',
|
||||
down: 'Hentikan',
|
||||
up: 'Mulakan',
|
||||
sure: 'Sahkan',
|
||||
},
|
||||
operate: {
|
||||
start: 'Mula',
|
||||
|
@ -1320,6 +1321,7 @@ const message = {
|
|||
errLog: 'Log Ralat',
|
||||
},
|
||||
file: {
|
||||
currentDir: 'Direktori Semasa',
|
||||
dir: 'Folder',
|
||||
upload: 'Muat naik',
|
||||
uploadFile: 'Muat naik fail',
|
||||
|
|
|
@ -77,6 +77,7 @@ const message = {
|
|||
fix: 'Corrigir',
|
||||
down: 'Parar',
|
||||
up: 'Iniciar',
|
||||
sure: 'Confirmar',
|
||||
},
|
||||
operate: {
|
||||
start: 'Iniciar',
|
||||
|
@ -1304,6 +1305,7 @@ const message = {
|
|||
errLog: 'Logs de erro',
|
||||
},
|
||||
file: {
|
||||
currentDir: 'Diretório atual',
|
||||
dir: 'Pasta',
|
||||
upload: 'Carregar',
|
||||
uploadFile: '@:file.upload @.lower:file.file',
|
||||
|
|
|
@ -77,6 +77,7 @@ const message = {
|
|||
fix: 'Исправить',
|
||||
down: 'Остановить',
|
||||
up: 'Запустить',
|
||||
sure: 'Подтвердить',
|
||||
},
|
||||
operate: {
|
||||
start: 'Запустить',
|
||||
|
@ -1308,6 +1309,7 @@ const message = {
|
|||
errLog: 'Логи ошибок',
|
||||
},
|
||||
file: {
|
||||
currentDir: 'Текущий каталог',
|
||||
dir: 'Папка',
|
||||
upload: 'Загрузить',
|
||||
uploadFile: '@:file.upload @.lower:file.file',
|
||||
|
|
|
@ -80,6 +80,7 @@ const message = {
|
|||
fix: '修復',
|
||||
down: '停止',
|
||||
up: '啟動',
|
||||
sure: '確定',
|
||||
},
|
||||
operate: {
|
||||
start: '啟動',
|
||||
|
@ -1270,6 +1271,7 @@ const message = {
|
|||
taskRunning: '運行中',
|
||||
},
|
||||
file: {
|
||||
currentDir: '當前目錄',
|
||||
dir: '文件夾',
|
||||
upload: '上傳',
|
||||
download: '下載',
|
||||
|
|
|
@ -80,6 +80,7 @@ const message = {
|
|||
fix: '修复',
|
||||
down: '停止',
|
||||
up: '启动',
|
||||
sure: '确定',
|
||||
},
|
||||
operate: {
|
||||
start: '启动',
|
||||
|
@ -1268,6 +1269,7 @@ const message = {
|
|||
taskRunning: '执行中',
|
||||
},
|
||||
file: {
|
||||
currentDir: '当前目录',
|
||||
dir: '文件夹',
|
||||
fileName: '文件名',
|
||||
search: '在当前目录下查找',
|
||||
|
|
|
@ -30,10 +30,12 @@
|
|||
<template #content>
|
||||
<div ref="dialogForm" class="px-4 py-2">
|
||||
<div class="flex justify-start items-center gap-x-4 card-action">
|
||||
<el-text @click="handleReset">{{ $t('commons.button.reset') }}</el-text>
|
||||
<el-text @click="saveContent()" class="ml-0">{{ $t('commons.button.save') }}</el-text>
|
||||
<el-text class="cursor-pointer" @click="handleReset">{{ $t('commons.button.reset') }}</el-text>
|
||||
<el-text class="cursor-pointer ml-0" @click="saveContent()">
|
||||
{{ $t('commons.button.save') }}
|
||||
</el-text>
|
||||
<el-dropdown trigger="click" max-height="300" placement="bottom-start" @command="changeTheme">
|
||||
<span class="el-dropdown-link">{{ $t('file.theme') }}</span>
|
||||
<span class="el-dropdown-link cursor-pointer">{{ $t('file.theme') }}</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="item in themes" :key="item.label" :command="item.value">
|
||||
|
@ -46,7 +48,7 @@
|
|||
</template>
|
||||
</el-dropdown>
|
||||
<el-dropdown trigger="click" max-height="300" placement="bottom-start" @command="changeLanguage">
|
||||
<span class="el-dropdown-link">{{ $t('file.language') }}</span>
|
||||
<span class="el-dropdown-link cursor-pointer">{{ $t('file.language') }}</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="item in Languages" :key="item.label" :command="item.label">
|
||||
|
@ -59,7 +61,7 @@
|
|||
</template>
|
||||
</el-dropdown>
|
||||
<el-dropdown trigger="click" max-height="300" placement="bottom-start" @command="changeEOL">
|
||||
<span class="el-dropdown-link">{{ $t('file.eol') }}</span>
|
||||
<span class="el-dropdown-link cursor-pointer">{{ $t('file.eol') }}</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="item in eols" :key="item.label" :command="item.value">
|
||||
|
@ -72,7 +74,7 @@
|
|||
</template>
|
||||
</el-dropdown>
|
||||
<el-dropdown trigger="click" max-height="300" placement="bottom-start">
|
||||
<span class="el-dropdown-link">{{ $t('file.setting') }}</span>
|
||||
<span class="el-dropdown-link cursor-pointer">{{ $t('file.setting') }}</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="changeMinimap(!config.minimap)">
|
||||
|
@ -92,7 +94,7 @@
|
|||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div v-loading="loading" class="">
|
||||
<div v-loading="loading">
|
||||
<div class="flex">
|
||||
<div
|
||||
class="monaco-editor sm:w-48 w-1/3 monaco-editor-background border-0 tree-container"
|
||||
|
@ -182,10 +184,13 @@
|
|||
>
|
||||
<DArrowRight />
|
||||
</el-icon>
|
||||
<div class="flex justify-center items-center h-full" v-if="fileTabs.length === 0">
|
||||
<el-empty :image="noUpdateImage" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-footer pl-4 h-6 flex justify-end items-center gap-4 rounded-b">
|
||||
<div class="code-footer pl-4 h-6 flex justify-end items-center gap-4 rounded-b" ref="dialogFooter">
|
||||
<el-divider direction="vertical" class="!h-6" v-if="config.theme" />
|
||||
<el-dropdown trigger="click" max-height="300" placement="top" @command="changeTheme">
|
||||
<span class="el-dropdown-link">
|
||||
|
@ -303,7 +308,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { getFileContent, getFilesTree, saveFileContent } from '@/api/modules/files';
|
||||
import i18n from '@/lang';
|
||||
import { MsgError, MsgInfo, MsgSuccess } from '@/utils/message';
|
||||
import { MsgError, MsgSuccess, MsgWarning } from '@/utils/message';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import { nextTick, onBeforeUnmount, reactive, ref, onMounted, computed } from 'vue';
|
||||
import { Languages } from '@/global/mimetype';
|
||||
|
@ -323,6 +328,7 @@ import { loadBaseDir } from '@/api/modules/setting';
|
|||
import { GlobalStore } from '@/store';
|
||||
import CodeTabs from './tabs/index.vue';
|
||||
import type { TabPaneName } from 'element-plus';
|
||||
import noUpdateImage from '@/assets/images/no_update_app.svg';
|
||||
|
||||
const codeTabsRef = ref();
|
||||
let editor: monaco.editor.IStandaloneCodeEditor | undefined;
|
||||
|
@ -496,6 +502,9 @@ const removeAllTab = (targetPath: string, type: string) => {
|
|||
const targetIndex = arr.findIndex((t) => t.path === targetPath);
|
||||
return index <= targetIndex;
|
||||
});
|
||||
} else if (type === 'all') {
|
||||
fileTabs.value = [];
|
||||
selectTab.value = '';
|
||||
}
|
||||
saveContent();
|
||||
})
|
||||
|
@ -524,9 +533,16 @@ const removeAllTab = (targetPath: string, type: string) => {
|
|||
const targetIndex = arr.findIndex((t) => t.path === targetPath);
|
||||
return index <= targetIndex;
|
||||
});
|
||||
} else if (type === 'all') {
|
||||
fileTabs.value = [];
|
||||
selectTab.value = '';
|
||||
}
|
||||
}
|
||||
getContent(selectTab.value, '');
|
||||
if (type === 'all') {
|
||||
editor.dispose();
|
||||
} else {
|
||||
getContent(selectTab.value, '');
|
||||
}
|
||||
};
|
||||
|
||||
const removeOtherTab = (targetPath: string) => {
|
||||
|
@ -648,7 +664,7 @@ const handleReset = () => {
|
|||
MsgSuccess(i18n.global.t('commons.msg.resetSuccess'));
|
||||
loading.value = false;
|
||||
} else {
|
||||
MsgInfo(i18n.global.t('file.noEdit'));
|
||||
MsgWarning(i18n.global.t('file.noEdit'));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -665,7 +681,7 @@ onMounted(() => {
|
|||
const updateHeights = () => {
|
||||
const vh = window.innerHeight / 100;
|
||||
if (isFullscreen.value) {
|
||||
let paddingHeight = 75;
|
||||
let paddingHeight = 30;
|
||||
const headerHeight = dialogHeader.value.offsetHeight;
|
||||
const formHeight = dialogForm.value.offsetHeight;
|
||||
const footerHeight = dialogFooter.value.offsetHeight;
|
||||
|
@ -789,7 +805,7 @@ const saveContent = () => {
|
|||
loading.value = false;
|
||||
});
|
||||
} else {
|
||||
MsgInfo(i18n.global.t('file.noEdit'));
|
||||
MsgWarning(i18n.global.t('file.noEdit'));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -920,7 +936,7 @@ const getContent = (path: string, extension: string) => {
|
|||
|
||||
if (isEdit.value) {
|
||||
ElMessageBox.confirm(i18n.global.t('file.saveAndOpenNewFile'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.open'),
|
||||
confirmButtonText: i18n.global.t('commons.button.sure'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'info',
|
||||
})
|
||||
|
@ -962,7 +978,7 @@ const search = async (path: string) => {
|
|||
|
||||
const getUpData = async () => {
|
||||
if ('/' === directoryPath.value) {
|
||||
MsgInfo(i18n.global.t('commons.msg.rootInfoErr'));
|
||||
MsgWarning(i18n.global.t('commons.msg.rootInfoErr'));
|
||||
return;
|
||||
}
|
||||
let pathParts = directoryPath.value.split('/');
|
||||
|
@ -1085,7 +1101,6 @@ defineExpose({ acceptParams });
|
|||
|
||||
:deep(.el-tabs) {
|
||||
--el-tabs-header-height: 28px;
|
||||
--el-text-color-primary: var(--el-text-color-regular);
|
||||
.el-tabs__header {
|
||||
height: 28px;
|
||||
margin: 0;
|
||||
|
@ -1094,11 +1109,25 @@ defineExpose({ acceptParams });
|
|||
height: 27px;
|
||||
line-height: 27px;
|
||||
}
|
||||
.el-tabs__nav {
|
||||
border-right: 1px solid var(--el-border-color-light);
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
border-bottom: none;
|
||||
border-radius: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.el-tabs__nav,
|
||||
.el-tabs__nav-next,
|
||||
.el-tabs__nav-prev {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
}
|
||||
.el-tabs__item.is-active {
|
||||
color: var(--el-color-primary);
|
||||
.el-dropdown {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<el-tabs
|
||||
v-model="props.selectTab"
|
||||
v-model="currentTab"
|
||||
type="card"
|
||||
:closable="props.fileTabs.length > 1"
|
||||
class="monaco-editor monaco-editor-background"
|
||||
|
@ -9,40 +9,49 @@
|
|||
>
|
||||
<el-tab-pane v-for="item in props.fileTabs" :key="item.path" :name="item.path">
|
||||
<template #label>
|
||||
<el-dropdown
|
||||
size="small"
|
||||
:id="item.path"
|
||||
:ref="(el) => setDropdownRef(item.path, el)"
|
||||
trigger="contextmenu"
|
||||
placement="bottom"
|
||||
@visible-change="(visible) => onDropdownVisibleChange(visible, item.path)"
|
||||
>
|
||||
<span class="el-dropdown-link">
|
||||
<el-tooltip :content="item.path" placement="bottom-start">
|
||||
{{ item.name }}
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="props.onRemoveTab(item.path)">
|
||||
<el-icon><Close /></el-icon>
|
||||
{{ $t('commons.button.close') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="props.onRemoveAllTab(item.path, 'left')">
|
||||
<el-icon><DArrowLeft /></el-icon>
|
||||
{{ $t('tabs.closeLeft') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="props.onRemoveAllTab(item.path, 'right')">
|
||||
<el-icon><DArrowRight /></el-icon>
|
||||
{{ $t('tabs.closeRight') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="props.onRemoveOtherTab(item.path)">
|
||||
<el-icon><More /></el-icon>
|
||||
{{ $t('tabs.closeOther') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-tooltip v-if="props.fileTabs.length == 1" :content="item.path" placement="bottom-start">
|
||||
<span>{{ item.name }}</span>
|
||||
</el-tooltip>
|
||||
<template v-if="props.fileTabs.length > 1">
|
||||
<el-dropdown
|
||||
size="small"
|
||||
:id="item.path"
|
||||
:ref="(el) => setDropdownRef(item.path, el)"
|
||||
trigger="contextmenu"
|
||||
placement="bottom"
|
||||
@visible-change="(visible) => onDropdownVisibleChange(visible, item.path)"
|
||||
>
|
||||
<span class="el-dropdown-link">
|
||||
<el-tooltip :content="item.path" placement="bottom-start">
|
||||
{{ item.name }}
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="props.onRemoveTab(item.path)">
|
||||
<el-icon><Close /></el-icon>
|
||||
{{ $t('commons.button.close') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="props.onRemoveAllTab(item.path, 'left')">
|
||||
<el-icon><DArrowLeft /></el-icon>
|
||||
{{ $t('tabs.closeLeft') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="props.onRemoveAllTab(item.path, 'right')">
|
||||
<el-icon><DArrowRight /></el-icon>
|
||||
{{ $t('tabs.closeRight') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="props.onRemoveOtherTab(item.path)">
|
||||
<el-icon><More /></el-icon>
|
||||
{{ $t('tabs.closeOther') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="props.onRemoveAllTab(item.path, 'all')">
|
||||
<el-icon><Operation /></el-icon>
|
||||
{{ $t('tabs.closeAll') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
@ -72,6 +81,15 @@ const setDropdownRef = (path: string, el: any) => {
|
|||
}
|
||||
};
|
||||
|
||||
const currentTab = ref(props.selectTab);
|
||||
|
||||
watch(
|
||||
() => props.selectTab,
|
||||
(val) => {
|
||||
currentTab.value = val;
|
||||
},
|
||||
);
|
||||
|
||||
const onDropdownVisibleChange = (visible: boolean, currentPath: string) => {
|
||||
if (visible) {
|
||||
for (const path in dropdownRefs.value) {
|
||||
|
|
|
@ -252,6 +252,9 @@
|
|||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
<el-button class="btn" @click="calculateSize(req.path)" :disabled="disableBtn">
|
||||
{{ $t('file.calculate') }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
|
||||
<el-badge :value="processCount" class="btn" v-if="processCount > 0">
|
||||
|
@ -432,7 +435,7 @@
|
|||
type="primary"
|
||||
link
|
||||
small
|
||||
@click="getDirSize(row, $index)"
|
||||
@click="getDirSize(row.path, $index)"
|
||||
:loading="row.btnLoading"
|
||||
>
|
||||
<span v-if="row.dirSize == undefined">
|
||||
|
@ -461,6 +464,16 @@
|
|||
width="270"
|
||||
fix
|
||||
/>
|
||||
<template #paginationLeft>
|
||||
<el-button type="primary" link small @click="getDirTotalSize(req.path)">
|
||||
<span v-if="dirTotalSize == -1">
|
||||
{{ $t('file.calculate') }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('file.currentDir') + $t('file.size') + ': ' + getFileSize(dirTotalSize) }}
|
||||
</span>
|
||||
</el-button>
|
||||
</template>
|
||||
</ComplexTable>
|
||||
</template>
|
||||
|
||||
|
@ -492,6 +505,7 @@
|
|||
import { computed, nextTick, onMounted, reactive, ref } from '@vue/runtime-core';
|
||||
import {
|
||||
addFavorite,
|
||||
computeDepthDirSize,
|
||||
computeDirSize,
|
||||
fileWgetKeys,
|
||||
getFileContent,
|
||||
|
@ -602,6 +616,8 @@ const previewRef = ref();
|
|||
const processRef = ref();
|
||||
const hostMount = ref<Dashboard.DiskInfo[]>([]);
|
||||
let resizeObserver: ResizeObserver;
|
||||
const dirTotalSize = ref(-1);
|
||||
const disableBtn = ref(false);
|
||||
|
||||
const { searchableStatus, searchablePath, searchableInputRef, searchableInputBlur } = useSearchable(paths);
|
||||
|
||||
|
@ -617,6 +633,7 @@ const mobile = computed(() => {
|
|||
});
|
||||
|
||||
const search = async () => {
|
||||
dirTotalSize.value = -1;
|
||||
getWgetProcess();
|
||||
loading.value = true;
|
||||
if (req.search != '') {
|
||||
|
@ -638,6 +655,7 @@ const search = async () => {
|
|||
|
||||
const searchFile = async () => {
|
||||
loading.value = true;
|
||||
dirTotalSize.value = -1;
|
||||
try {
|
||||
return await getFilesList(req);
|
||||
} finally {
|
||||
|
@ -866,9 +884,9 @@ const getFileSize = (size: number) => {
|
|||
return computeSize(size);
|
||||
};
|
||||
|
||||
const getDirSize = async (row: any, index: number) => {
|
||||
const getDirSize = async (path: string, index: number) => {
|
||||
const req = {
|
||||
path: row.path,
|
||||
path: path,
|
||||
};
|
||||
data.value[index].btnLoading = true;
|
||||
await computeDirSize(req)
|
||||
|
@ -882,6 +900,34 @@ const getDirSize = async (row: any, index: number) => {
|
|||
});
|
||||
};
|
||||
|
||||
const getDirTotalSize = async (path: string) => {
|
||||
const req = {
|
||||
path: path,
|
||||
};
|
||||
const res = await computeDirSize(req);
|
||||
dirTotalSize.value = res.data.size;
|
||||
};
|
||||
|
||||
const calculateSize = (path: string) => {
|
||||
const req = { path };
|
||||
disableBtn.value = true;
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const res = await computeDepthDirSize(req);
|
||||
const sizeMap = new Map(res.data.map((dir) => [dir.path, dir.size]));
|
||||
data.value.forEach((item) => {
|
||||
if (sizeMap.has(item.path)) {
|
||||
item.dirSize = sizeMap.get(item.path)!;
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error computing dir size:', err);
|
||||
} finally {
|
||||
disableBtn.value = false;
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const getIconName = (extension: string) => {
|
||||
return getIcon(extension);
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue