feat: Multi directory calculation size (#8762)

* feat: Multi directory calculation size

* feat: optimize code editor

* feat: directory calculation size
This commit is contained in:
2025-05-21 11:55:37 +08:00 committed by GitHub
parent 58d9c068e4
commit 057dcaf2e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 297 additions and 59 deletions

View file

@ -628,6 +628,28 @@ func (b *BaseApi) Size(c *gin.Context) {
helper.SuccessWithData(c, res) 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 { func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int, overwrite bool) error {
defer func() { defer func() {
_ = os.RemoveAll(fileDir) _ = os.RemoveAll(fileDir)

View file

@ -64,3 +64,8 @@ type UserGroupResponse struct {
Users []UserInfo `json:"users"` Users []UserInfo `json:"users"`
Groups []string `json:"groups"` Groups []string `json:"groups"`
} }
type DepthDirSizeRes struct {
Path string `json:"path"`
Size int64 `json:"size"`
}

View file

@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"context" "context"
"fmt" "fmt"
"github.com/jinzhu/copier"
"io" "io"
"io/fs" "io/fs"
"os" "os"
@ -51,6 +52,7 @@ type IFileService interface {
SaveContent(edit request.FileEdit) error SaveContent(edit request.FileEdit) error
FileDownload(d request.FileDownload) (string, error) FileDownload(d request.FileDownload) (string, error)
DirSize(req request.DirSizeReq) (response.DirSizeRes, error) DirSize(req request.DirSizeReq) (response.DirSizeRes, error)
DepthDirSize(req request.DirSizeReq) ([]response.DepthDirSizeRes, error)
ChangeName(req request.FileRename) error ChangeName(req request.FileRename) error
Wget(w request.FileWget) (string, error) Wget(w request.FileWget) (string, error)
MvFile(m request.FileMove) error MvFile(m request.FileMove) error
@ -443,6 +445,22 @@ func (f *FileService) DirSize(req request.DirSizeReq) (response.DirSizeRes, erro
return res, nil 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) { func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.FileLineContent, error) {
logFilePath := "" logFilePath := ""
switch req.Type { switch req.Type {

View file

@ -34,6 +34,7 @@ func (f *FileRouter) InitRouter(Router *gin.RouterGroup) {
fileRouter.GET("/download", baseApi.Download) fileRouter.GET("/download", baseApi.Download)
fileRouter.POST("/chunkdownload", baseApi.DownloadChunkFiles) fileRouter.POST("/chunkdownload", baseApi.DownloadChunkFiles)
fileRouter.POST("/size", baseApi.Size) fileRouter.POST("/size", baseApi.Size)
fileRouter.POST("/depth/size", baseApi.DepthDirSize)
fileRouter.GET("/wget/process", baseApi.WgetProcess) fileRouter.GET("/wget/process", baseApi.WgetProcess)
fileRouter.GET("/wget/process/keys", baseApi.ProcessKeys) fileRouter.GET("/wget/process/keys", baseApi.ProcessKeys)
fileRouter.POST("/read", baseApi.ReadFileByLine) fileRouter.POST("/read", baseApi.ReadFileByLine)

View file

@ -512,6 +512,71 @@ func (f FileOp) GetDirSize(path string) (int64, error) {
return size, nil 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 { func getFormat(cType CompressType) archiver.CompressedArchive {
format := archiver.CompressedArchive{} format := archiver.CompressedArchive{}
switch cType { switch cType {

View file

@ -152,6 +152,11 @@ export namespace File {
size: number; size: number;
} }
export interface DepthDirSizeRes {
size: number;
path: string;
}
export interface FilePath { export interface FilePath {
path: string; path: string;
} }

View file

@ -98,6 +98,10 @@ export const computeDirSize = (params: File.DirSizeReq) => {
return http.post<File.DirSizeRes>('files/size', params, TimeoutEnum.T_5M); 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 = () => { export const fileWgetKeys = () => {
return http.get<File.FileKeys>('files/wget/process/keys'); return http.get<File.FileKeys>('files/wget/process/keys');
}; };

View file

@ -21,7 +21,12 @@
</fu-table> </fu-table>
</div> </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"> <slot name="pagination">
<el-pagination <el-pagination
v-model:current-page="paginationConfig.currentPage" v-model:current-page="paginationConfig.currentPage"
@ -100,21 +105,25 @@ defineExpose({
onMounted(() => { onMounted(() => {
let heightDiff = 320; let heightDiff = 320;
let tabHeight = 0;
if (props.heightDiff) { if (props.heightDiff) {
heightDiff = props.heightDiff; heightDiff = props.heightDiff;
} }
if (globalStore.openMenuTabs) {
tabHeight = 48;
}
if (props.height) { if (props.height) {
tableHeight.value = props.height; tableHeight.value = props.height - tabHeight;
} else { } else {
tableHeight.value = window.innerHeight - heightDiff; tableHeight.value = window.innerHeight - heightDiff - tabHeight;
} }
window.onresize = () => { window.onresize = () => {
return (() => { return (() => {
if (props.height) { if (props.height) {
tableHeight.value = props.height; tableHeight.value = props.height - tabHeight;
} else { } else {
tableHeight.value = window.innerHeight - heightDiff; tableHeight.value = window.innerHeight - heightDiff - tabHeight;
} }
})(); })();
}; };

View file

@ -80,6 +80,7 @@ const message = {
fix: 'Fix', fix: 'Fix',
down: 'Stop', down: 'Stop',
up: 'Start', up: 'Start',
sure: 'Confirm',
}, },
operate: { operate: {
start: 'Start', start: 'Start',
@ -1342,6 +1343,7 @@ const message = {
taskRunning: 'Running', taskRunning: 'Running',
}, },
file: { file: {
currentDir: 'Directory',
dir: 'Folder', dir: 'Folder',
fileName: 'File name', fileName: 'File name',
search: 'Find', search: 'Find',

View file

@ -77,6 +77,7 @@ const message = {
fix: '修正', fix: '修正',
down: '停止', down: '停止',
up: '起動', up: '起動',
sure: '確認',
}, },
operate: { operate: {
start: '開始', start: '開始',
@ -1278,6 +1279,7 @@ const message = {
errLog: 'エラーログ', errLog: 'エラーログ',
}, },
file: { file: {
currentDir: '現在のディレクトリ',
dir: 'フォルダ', dir: 'フォルダ',
upload: 'アップロード', upload: 'アップロード',
uploadFile: '@:file.upload@.lower:file.file', uploadFile: '@:file.upload@.lower:file.file',

View file

@ -77,6 +77,7 @@ const message = {
fix: '수정', fix: '수정',
down: '중지', down: '중지',
up: '시작', up: '시작',
sure: '확인',
}, },
operate: { operate: {
start: '시작', start: '시작',
@ -1265,6 +1266,7 @@ const message = {
errLog: '에러 로그', errLog: '에러 로그',
}, },
file: { file: {
currentDir: '현재 디렉터리',
dir: '폴더', dir: '폴더',
upload: '업로드', upload: '업로드',
uploadFile: '@:file.upload @.lower:file.file', uploadFile: '@:file.upload @.lower:file.file',

View file

@ -77,6 +77,7 @@ const message = {
fix: 'Betulkan', fix: 'Betulkan',
down: 'Hentikan', down: 'Hentikan',
up: 'Mulakan', up: 'Mulakan',
sure: 'Sahkan',
}, },
operate: { operate: {
start: 'Mula', start: 'Mula',
@ -1320,6 +1321,7 @@ const message = {
errLog: 'Log Ralat', errLog: 'Log Ralat',
}, },
file: { file: {
currentDir: 'Direktori Semasa',
dir: 'Folder', dir: 'Folder',
upload: 'Muat naik', upload: 'Muat naik',
uploadFile: 'Muat naik fail', uploadFile: 'Muat naik fail',

View file

@ -77,6 +77,7 @@ const message = {
fix: 'Corrigir', fix: 'Corrigir',
down: 'Parar', down: 'Parar',
up: 'Iniciar', up: 'Iniciar',
sure: 'Confirmar',
}, },
operate: { operate: {
start: 'Iniciar', start: 'Iniciar',
@ -1304,6 +1305,7 @@ const message = {
errLog: 'Logs de erro', errLog: 'Logs de erro',
}, },
file: { file: {
currentDir: 'Diretório atual',
dir: 'Pasta', dir: 'Pasta',
upload: 'Carregar', upload: 'Carregar',
uploadFile: '@:file.upload @.lower:file.file', uploadFile: '@:file.upload @.lower:file.file',

View file

@ -77,6 +77,7 @@ const message = {
fix: 'Исправить', fix: 'Исправить',
down: 'Остановить', down: 'Остановить',
up: 'Запустить', up: 'Запустить',
sure: 'Подтвердить',
}, },
operate: { operate: {
start: 'Запустить', start: 'Запустить',
@ -1308,6 +1309,7 @@ const message = {
errLog: 'Логи ошибок', errLog: 'Логи ошибок',
}, },
file: { file: {
currentDir: 'Текущий каталог',
dir: 'Папка', dir: 'Папка',
upload: 'Загрузить', upload: 'Загрузить',
uploadFile: '@:file.upload @.lower:file.file', uploadFile: '@:file.upload @.lower:file.file',

View file

@ -80,6 +80,7 @@ const message = {
fix: '修復', fix: '修復',
down: '停止', down: '停止',
up: '啟動', up: '啟動',
sure: '確定',
}, },
operate: { operate: {
start: '啟動', start: '啟動',
@ -1270,6 +1271,7 @@ const message = {
taskRunning: '運行中', taskRunning: '運行中',
}, },
file: { file: {
currentDir: '當前目錄',
dir: '文件夾', dir: '文件夾',
upload: '上傳', upload: '上傳',
download: '下載', download: '下載',

View file

@ -80,6 +80,7 @@ const message = {
fix: '修复', fix: '修复',
down: '停止', down: '停止',
up: '启动', up: '启动',
sure: '确定',
}, },
operate: { operate: {
start: '启动', start: '启动',
@ -1268,6 +1269,7 @@ const message = {
taskRunning: '执行中', taskRunning: '执行中',
}, },
file: { file: {
currentDir: '当前目录',
dir: '文件夹', dir: '文件夹',
fileName: '文件名', fileName: '文件名',
search: '在当前目录下查找', search: '在当前目录下查找',

View file

@ -30,10 +30,12 @@
<template #content> <template #content>
<div ref="dialogForm" class="px-4 py-2"> <div ref="dialogForm" class="px-4 py-2">
<div class="flex justify-start items-center gap-x-4 card-action"> <div class="flex justify-start items-center gap-x-4 card-action">
<el-text @click="handleReset">{{ $t('commons.button.reset') }}</el-text> <el-text class="cursor-pointer" @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 ml-0" @click="saveContent()">
{{ $t('commons.button.save') }}
</el-text>
<el-dropdown trigger="click" max-height="300" placement="bottom-start" @command="changeTheme"> <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> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item v-for="item in themes" :key="item.label" :command="item.value"> <el-dropdown-item v-for="item in themes" :key="item.label" :command="item.value">
@ -46,7 +48,7 @@
</template> </template>
</el-dropdown> </el-dropdown>
<el-dropdown trigger="click" max-height="300" placement="bottom-start" @command="changeLanguage"> <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> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item v-for="item in Languages" :key="item.label" :command="item.label"> <el-dropdown-item v-for="item in Languages" :key="item.label" :command="item.label">
@ -59,7 +61,7 @@
</template> </template>
</el-dropdown> </el-dropdown>
<el-dropdown trigger="click" max-height="300" placement="bottom-start" @command="changeEOL"> <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> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item v-for="item in eols" :key="item.label" :command="item.value"> <el-dropdown-item v-for="item in eols" :key="item.label" :command="item.value">
@ -72,7 +74,7 @@
</template> </template>
</el-dropdown> </el-dropdown>
<el-dropdown trigger="click" max-height="300" placement="bottom-start"> <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> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item @click="changeMinimap(!config.minimap)"> <el-dropdown-item @click="changeMinimap(!config.minimap)">
@ -92,7 +94,7 @@
</el-dropdown> </el-dropdown>
</div> </div>
</div> </div>
<div v-loading="loading" class=""> <div v-loading="loading">
<div class="flex"> <div class="flex">
<div <div
class="monaco-editor sm:w-48 w-1/3 monaco-editor-background border-0 tree-container" class="monaco-editor sm:w-48 w-1/3 monaco-editor-background border-0 tree-container"
@ -182,10 +184,13 @@
> >
<DArrowRight /> <DArrowRight />
</el-icon> </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>
</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-divider direction="vertical" class="!h-6" v-if="config.theme" />
<el-dropdown trigger="click" max-height="300" placement="top" @command="changeTheme"> <el-dropdown trigger="click" max-height="300" placement="top" @command="changeTheme">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
@ -303,7 +308,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { getFileContent, getFilesTree, saveFileContent } from '@/api/modules/files'; import { getFileContent, getFilesTree, saveFileContent } from '@/api/modules/files';
import i18n from '@/lang'; 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 * as monaco from 'monaco-editor';
import { nextTick, onBeforeUnmount, reactive, ref, onMounted, computed } from 'vue'; import { nextTick, onBeforeUnmount, reactive, ref, onMounted, computed } from 'vue';
import { Languages } from '@/global/mimetype'; import { Languages } from '@/global/mimetype';
@ -323,6 +328,7 @@ import { loadBaseDir } from '@/api/modules/setting';
import { GlobalStore } from '@/store'; import { GlobalStore } from '@/store';
import CodeTabs from './tabs/index.vue'; import CodeTabs from './tabs/index.vue';
import type { TabPaneName } from 'element-plus'; import type { TabPaneName } from 'element-plus';
import noUpdateImage from '@/assets/images/no_update_app.svg';
const codeTabsRef = ref(); const codeTabsRef = ref();
let editor: monaco.editor.IStandaloneCodeEditor | undefined; let editor: monaco.editor.IStandaloneCodeEditor | undefined;
@ -496,6 +502,9 @@ const removeAllTab = (targetPath: string, type: string) => {
const targetIndex = arr.findIndex((t) => t.path === targetPath); const targetIndex = arr.findIndex((t) => t.path === targetPath);
return index <= targetIndex; return index <= targetIndex;
}); });
} else if (type === 'all') {
fileTabs.value = [];
selectTab.value = '';
} }
saveContent(); saveContent();
}) })
@ -524,9 +533,16 @@ const removeAllTab = (targetPath: string, type: string) => {
const targetIndex = arr.findIndex((t) => t.path === targetPath); const targetIndex = arr.findIndex((t) => t.path === targetPath);
return index <= targetIndex; 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) => { const removeOtherTab = (targetPath: string) => {
@ -648,7 +664,7 @@ const handleReset = () => {
MsgSuccess(i18n.global.t('commons.msg.resetSuccess')); MsgSuccess(i18n.global.t('commons.msg.resetSuccess'));
loading.value = false; loading.value = false;
} else { } else {
MsgInfo(i18n.global.t('file.noEdit')); MsgWarning(i18n.global.t('file.noEdit'));
} }
}; };
@ -665,7 +681,7 @@ onMounted(() => {
const updateHeights = () => { const updateHeights = () => {
const vh = window.innerHeight / 100; const vh = window.innerHeight / 100;
if (isFullscreen.value) { if (isFullscreen.value) {
let paddingHeight = 75; let paddingHeight = 30;
const headerHeight = dialogHeader.value.offsetHeight; const headerHeight = dialogHeader.value.offsetHeight;
const formHeight = dialogForm.value.offsetHeight; const formHeight = dialogForm.value.offsetHeight;
const footerHeight = dialogFooter.value.offsetHeight; const footerHeight = dialogFooter.value.offsetHeight;
@ -789,7 +805,7 @@ const saveContent = () => {
loading.value = false; loading.value = false;
}); });
} else { } 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) { if (isEdit.value) {
ElMessageBox.confirm(i18n.global.t('file.saveAndOpenNewFile'), { 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'), cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info', type: 'info',
}) })
@ -962,7 +978,7 @@ const search = async (path: string) => {
const getUpData = async () => { const getUpData = async () => {
if ('/' === directoryPath.value) { if ('/' === directoryPath.value) {
MsgInfo(i18n.global.t('commons.msg.rootInfoErr')); MsgWarning(i18n.global.t('commons.msg.rootInfoErr'));
return; return;
} }
let pathParts = directoryPath.value.split('/'); let pathParts = directoryPath.value.split('/');
@ -1085,7 +1101,6 @@ defineExpose({ acceptParams });
:deep(.el-tabs) { :deep(.el-tabs) {
--el-tabs-header-height: 28px; --el-tabs-header-height: 28px;
--el-text-color-primary: var(--el-text-color-regular);
.el-tabs__header { .el-tabs__header {
height: 28px; height: 28px;
margin: 0; margin: 0;
@ -1094,11 +1109,25 @@ defineExpose({ acceptParams });
height: 27px; height: 27px;
line-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,
.el-tabs__nav-next, .el-tabs__nav-next,
.el-tabs__nav-prev { .el-tabs__nav-prev {
height: 28px; height: 28px;
line-height: 28px; line-height: 28px;
} }
.el-tabs__item.is-active {
color: var(--el-color-primary);
.el-dropdown {
color: var(--el-color-primary);
}
}
} }
</style> </style>

View file

@ -1,6 +1,6 @@
<template> <template>
<el-tabs <el-tabs
v-model="props.selectTab" v-model="currentTab"
type="card" type="card"
:closable="props.fileTabs.length > 1" :closable="props.fileTabs.length > 1"
class="monaco-editor monaco-editor-background" 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"> <el-tab-pane v-for="item in props.fileTabs" :key="item.path" :name="item.path">
<template #label> <template #label>
<el-dropdown <el-tooltip v-if="props.fileTabs.length == 1" :content="item.path" placement="bottom-start">
size="small" <span>{{ item.name }}</span>
:id="item.path" </el-tooltip>
:ref="(el) => setDropdownRef(item.path, el)" <template v-if="props.fileTabs.length > 1">
trigger="contextmenu" <el-dropdown
placement="bottom" size="small"
@visible-change="(visible) => onDropdownVisibleChange(visible, item.path)" :id="item.path"
> :ref="(el) => setDropdownRef(item.path, el)"
<span class="el-dropdown-link"> trigger="contextmenu"
<el-tooltip :content="item.path" placement="bottom-start"> placement="bottom"
{{ item.name }} @visible-change="(visible) => onDropdownVisibleChange(visible, item.path)"
</el-tooltip> >
</span> <span class="el-dropdown-link">
<template #dropdown> <el-tooltip :content="item.path" placement="bottom-start">
<el-dropdown-menu> {{ item.name }}
<el-dropdown-item @click="props.onRemoveTab(item.path)"> </el-tooltip>
<el-icon><Close /></el-icon> </span>
{{ $t('commons.button.close') }} <template #dropdown>
</el-dropdown-item> <el-dropdown-menu>
<el-dropdown-item @click="props.onRemoveAllTab(item.path, 'left')"> <el-dropdown-item @click="props.onRemoveTab(item.path)">
<el-icon><DArrowLeft /></el-icon> <el-icon><Close /></el-icon>
{{ $t('tabs.closeLeft') }} {{ $t('commons.button.close') }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item @click="props.onRemoveAllTab(item.path, 'right')"> <el-dropdown-item @click="props.onRemoveAllTab(item.path, 'left')">
<el-icon><DArrowRight /></el-icon> <el-icon><DArrowLeft /></el-icon>
{{ $t('tabs.closeRight') }} {{ $t('tabs.closeLeft') }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item @click="props.onRemoveOtherTab(item.path)"> <el-dropdown-item @click="props.onRemoveAllTab(item.path, 'right')">
<el-icon><More /></el-icon> <el-icon><DArrowRight /></el-icon>
{{ $t('tabs.closeOther') }} {{ $t('tabs.closeRight') }}
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> <el-dropdown-item @click="props.onRemoveOtherTab(item.path)">
</template> <el-icon><More /></el-icon>
</el-dropdown> {{ $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> </template>
</el-tab-pane> </el-tab-pane>
</el-tabs> </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) => { const onDropdownVisibleChange = (visible: boolean, currentPath: string) => {
if (visible) { if (visible) {
for (const path in dropdownRefs.value) { for (const path in dropdownRefs.value) {

View file

@ -252,6 +252,9 @@
</template> </template>
</el-dropdown> </el-dropdown>
</template> </template>
<el-button class="btn" @click="calculateSize(req.path)" :disabled="disableBtn">
{{ $t('file.calculate') }}
</el-button>
</el-button-group> </el-button-group>
<el-badge :value="processCount" class="btn" v-if="processCount > 0"> <el-badge :value="processCount" class="btn" v-if="processCount > 0">
@ -432,7 +435,7 @@
type="primary" type="primary"
link link
small small
@click="getDirSize(row, $index)" @click="getDirSize(row.path, $index)"
:loading="row.btnLoading" :loading="row.btnLoading"
> >
<span v-if="row.dirSize == undefined"> <span v-if="row.dirSize == undefined">
@ -461,6 +464,16 @@
width="270" width="270"
fix 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> </ComplexTable>
</template> </template>
@ -492,6 +505,7 @@
import { computed, nextTick, onMounted, reactive, ref } from '@vue/runtime-core'; import { computed, nextTick, onMounted, reactive, ref } from '@vue/runtime-core';
import { import {
addFavorite, addFavorite,
computeDepthDirSize,
computeDirSize, computeDirSize,
fileWgetKeys, fileWgetKeys,
getFileContent, getFileContent,
@ -602,6 +616,8 @@ const previewRef = ref();
const processRef = ref(); const processRef = ref();
const hostMount = ref<Dashboard.DiskInfo[]>([]); const hostMount = ref<Dashboard.DiskInfo[]>([]);
let resizeObserver: ResizeObserver; let resizeObserver: ResizeObserver;
const dirTotalSize = ref(-1);
const disableBtn = ref(false);
const { searchableStatus, searchablePath, searchableInputRef, searchableInputBlur } = useSearchable(paths); const { searchableStatus, searchablePath, searchableInputRef, searchableInputBlur } = useSearchable(paths);
@ -617,6 +633,7 @@ const mobile = computed(() => {
}); });
const search = async () => { const search = async () => {
dirTotalSize.value = -1;
getWgetProcess(); getWgetProcess();
loading.value = true; loading.value = true;
if (req.search != '') { if (req.search != '') {
@ -638,6 +655,7 @@ const search = async () => {
const searchFile = async () => { const searchFile = async () => {
loading.value = true; loading.value = true;
dirTotalSize.value = -1;
try { try {
return await getFilesList(req); return await getFilesList(req);
} finally { } finally {
@ -866,9 +884,9 @@ const getFileSize = (size: number) => {
return computeSize(size); return computeSize(size);
}; };
const getDirSize = async (row: any, index: number) => { const getDirSize = async (path: string, index: number) => {
const req = { const req = {
path: row.path, path: path,
}; };
data.value[index].btnLoading = true; data.value[index].btnLoading = true;
await computeDirSize(req) 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) => { const getIconName = (extension: string) => {
return getIcon(extension); return getIcon(extension);
}; };