mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-11 07:55:59 +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)
|
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)
|
||||||
|
|
|
@ -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"`
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
};
|
};
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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: '下載',
|
||||||
|
|
|
@ -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: '在当前目录下查找',
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Reference in a new issue