mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2024-11-24 02:59:16 +08:00
feat: 文件管理编辑文件增加左侧目录树 (#5594)
Refs: #3624 #### What this PR does / why we need it? #### Summary of your change #### Please indicate you've done the following: - [ ] Made sure tests are passing and test coverage is added if needed. - [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/). - [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
This commit is contained in:
parent
3adf1aebb8
commit
9cc4ec49ef
@ -18,6 +18,8 @@ type FileTree struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
IsDir bool `json:"isDir"`
|
||||
Extension string `json:"extension"`
|
||||
Children []FileTree `json:"children"`
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/backend/buserr"
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
"golang.org/x/net/html/charset"
|
||||
"golang.org/x/sys/unix"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
"golang.org/x/text/transform"
|
||||
|
||||
@ -51,6 +52,10 @@ type IFileService interface {
|
||||
ReadLogByLine(req request.FileReadByLineReq) (*response.FileLineContent, error)
|
||||
}
|
||||
|
||||
var filteredPaths = []string{
|
||||
"/.1panel_clash",
|
||||
}
|
||||
|
||||
func NewIFileService() IFileService {
|
||||
return &FileService{}
|
||||
}
|
||||
@ -100,6 +105,9 @@ func (f *FileService) SearchUploadWithPage(req request.SearchUploadWithPage) (in
|
||||
|
||||
func (f *FileService) GetFileTree(op request.FileOption) ([]response.FileTree, error) {
|
||||
var treeArray []response.FileTree
|
||||
if _, err := os.Stat(op.Path); err != nil && os.IsNotExist(err) {
|
||||
return treeArray, nil
|
||||
}
|
||||
info, err := files.NewFileInfo(op.FileOption)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -108,17 +116,65 @@ func (f *FileService) GetFileTree(op request.FileOption) ([]response.FileTree, e
|
||||
ID: common.GetUuid(),
|
||||
Name: info.Name,
|
||||
Path: info.Path,
|
||||
IsDir: info.IsDir,
|
||||
Extension: info.Extension,
|
||||
}
|
||||
for _, v := range info.Items {
|
||||
if v.IsDir {
|
||||
node.Children = append(node.Children, response.FileTree{
|
||||
err = f.buildFileTree(&node, info.Items, op, 2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(treeArray, node), nil
|
||||
}
|
||||
|
||||
func shouldFilterPath(path string) bool {
|
||||
cleanedPath := filepath.Clean(path)
|
||||
for _, filteredPath := range filteredPaths {
|
||||
cleanedFilteredPath := filepath.Clean(filteredPath)
|
||||
if cleanedFilteredPath == cleanedPath || strings.HasPrefix(cleanedPath, cleanedFilteredPath+"/") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 递归构建文件树(只取当前目录以及当前目录下的第一层子节点)
|
||||
func (f *FileService) buildFileTree(node *response.FileTree, items []*files.FileInfo, op request.FileOption, level int) error {
|
||||
for _, v := range items {
|
||||
if shouldFilterPath(v.Path) {
|
||||
global.LOG.Info("File Tree: Skipping %s due to filter\n", v.Path)
|
||||
continue
|
||||
}
|
||||
childNode := response.FileTree{
|
||||
ID: common.GetUuid(),
|
||||
Name: v.Name,
|
||||
Path: v.Path,
|
||||
})
|
||||
IsDir: v.IsDir,
|
||||
Extension: v.Extension,
|
||||
}
|
||||
if level > 1 && v.IsDir {
|
||||
if err := f.buildChildNode(&childNode, v, op, level); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return append(treeArray, node), nil
|
||||
|
||||
node.Children = append(node.Children, childNode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FileService) buildChildNode(childNode *response.FileTree, fileInfo *files.FileInfo, op request.FileOption, level int) error {
|
||||
op.Path = fileInfo.Path
|
||||
subInfo, err := files.NewFileInfo(op.FileOption)
|
||||
if err != nil {
|
||||
if os.IsPermission(err) || errors.Is(err, unix.EACCES) {
|
||||
global.LOG.Info("File Tree: Skipping %s due to permission denied\n", fileInfo.Path)
|
||||
return nil
|
||||
}
|
||||
global.LOG.Errorf("File Tree: Skipping %s due to error: %s\n", fileInfo.Path, err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
return f.buildFileTree(childNode, subInfo.Items, op, level-1)
|
||||
}
|
||||
|
||||
func (f *FileService) Create(op request.FileCreate) error {
|
||||
|
@ -93,6 +93,7 @@ const message = {
|
||||
user: 'User',
|
||||
title: 'Title',
|
||||
port: 'Port',
|
||||
forward: 'Forward',
|
||||
protocol: 'Protocol',
|
||||
tableSetting: 'Table setting',
|
||||
refreshRate: 'Rate',
|
||||
@ -139,6 +140,8 @@ const message = {
|
||||
remove: 'Remove',
|
||||
backupHelper: 'The current operation will back up {0}. Do you want to proceed?',
|
||||
recoverHelper: 'Restoring from {0} file. This operation is irreversible. Do you want to continue?',
|
||||
refreshSuccess: 'Refresh successful',
|
||||
rootInfoErr: "It's already the root directory",
|
||||
},
|
||||
login: {
|
||||
username: 'UserName',
|
||||
@ -1220,6 +1223,7 @@ const message = {
|
||||
refresh: 'Refresh',
|
||||
openWithVscode: 'Open with VS Code',
|
||||
vscodeHelper: 'Please make sure that VS Code is installed locally and the SSH Remote plugin is configured',
|
||||
up: 'Go back',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: 'Auto Start',
|
||||
|
@ -92,6 +92,7 @@ const message = {
|
||||
user: '用戶',
|
||||
title: '標題',
|
||||
port: '端口',
|
||||
forward: '轉發',
|
||||
protocol: '協議',
|
||||
tableSetting: '列表設置',
|
||||
refreshRate: '刷新頻率',
|
||||
@ -139,6 +140,8 @@ const message = {
|
||||
remove: '移出',
|
||||
backupHelper: '當前操作將對 {0} 進行備份,是否繼續?',
|
||||
recoverHelper: '將從 {0} 文件進行恢復,該操作不可回滾,是否繼續?',
|
||||
refreshSuccess: '重繪成功',
|
||||
rootInfoErr: '已經是根目錄了',
|
||||
},
|
||||
login: {
|
||||
username: '用戶名',
|
||||
@ -1150,12 +1153,13 @@ const message = {
|
||||
'下載時忽略不可信證書可能導致數據洩露或篡改。請謹慎使用此選項,僅在信任下載源的情況下啟用',
|
||||
uploadOverLimit: '文件數量超過 1000! 請壓縮後上傳',
|
||||
clashDitNotSupport: '檔名禁止包含 .1panel_clash',
|
||||
clashDleteAlert: '回收站資料夾不能刪除',
|
||||
clashDeleteAlert: '回收站資料夾不能刪除',
|
||||
clashOpenAlert: '回收站目錄請點選【回收站】按鈕開啟',
|
||||
right: '前進',
|
||||
back: '後退',
|
||||
top: '返回上一層',
|
||||
refresh: '重新整理',
|
||||
up: '上一層',
|
||||
openWithVscode: 'VS Code 打開',
|
||||
vscodeHelper: '請確保本地已安裝 VS Code 並配置了 SSH Remote 插件',
|
||||
},
|
||||
|
@ -140,6 +140,8 @@ const message = {
|
||||
remove: '移出',
|
||||
backupHelper: '当前操作将对 {0} 进行备份,是否继续?',
|
||||
recoverHelper: '将从 {0} 文件进行恢复,该操作不可回滚,是否继续?',
|
||||
refreshSuccess: '刷新成功',
|
||||
rootInfoErr: '已经是根目录了',
|
||||
},
|
||||
login: {
|
||||
username: '用户名',
|
||||
@ -1152,12 +1154,13 @@ const message = {
|
||||
'下载时忽略不可信证书可能导致数据泄露或篡改。请谨慎使用此选项,仅在信任下载源的情况下启用',
|
||||
uploadOverLimit: '文件数量超过 1000!请压缩后上传',
|
||||
clashDitNotSupport: '文件名禁止包含 .1panel_clash',
|
||||
clashDleteAlert: '回收站文件夹不能删除',
|
||||
clashDeleteAlert: '回收站文件夹不能删除',
|
||||
clashOpenAlert: '回收站目录请点击【回收站】按钮打开',
|
||||
right: '前进',
|
||||
back: '后退',
|
||||
top: '返回上一级',
|
||||
refresh: '刷新',
|
||||
up: '上一级',
|
||||
openWithVscode: 'VS Code 打开',
|
||||
vscodeHelper: '请确保本地已安装 VS Code 并配置了 SSH Remote 插件',
|
||||
},
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="open"
|
||||
:title="$t('commons.button.edit') + ' - ' + fileName"
|
||||
:show-close="false"
|
||||
:before-close="handleClose"
|
||||
destroy-on-close
|
||||
width="70%"
|
||||
@ -9,10 +9,18 @@
|
||||
:top="'5vh'"
|
||||
:fullscreen="isFullscreen"
|
||||
>
|
||||
<el-form :inline="true" :model="config" class="mt-1.5">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>{{ $t('commons.button.edit') + ' - ' + form.path }}</span>
|
||||
<el-space alignment="center" :size="10" class="dialog-header-icon">
|
||||
<el-tooltip :content="loadTooltip()" placement="top">
|
||||
<el-button @click="toggleFullscreen" class="fullScreen" icon="FullScreen" plain></el-button>
|
||||
<el-icon @click="toggleFullscreen"><FullScreen /></el-icon>
|
||||
</el-tooltip>
|
||||
<el-icon @click="handleClose" size="20"><Close /></el-icon>
|
||||
</el-space>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :inline="true" :model="config" class="mt-1.5">
|
||||
<el-form-item :label="$t('file.theme')">
|
||||
<el-select v-model="config.theme" @change="changeTheme()" class="p-w-200">
|
||||
<el-option v-for="item in themes" :key="item.label" :value="item.value" :label="item.label" />
|
||||
@ -36,7 +44,90 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div v-loading="loading">
|
||||
<div id="codeBox" style="height: 55vh"></div>
|
||||
<div class="flex">
|
||||
<div class="sm:w-48 w-1/3 monaco-editor-background tree-container" v-if="isShow">
|
||||
<div class="flex items-center justify-between pl-1 sm:pr-4 pr-1 pt-1">
|
||||
<el-tooltip :content="$t('file.top')" placement="top">
|
||||
<el-text size="small" @click="getUpData()" class="cursor-pointer">
|
||||
<el-icon>
|
||||
<Top />
|
||||
</el-icon>
|
||||
<span class="sm:inline hidden pl-1">{{ $t('file.up') }}</span>
|
||||
</el-text>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip :content="$t('file.refresh')" placement="top">
|
||||
<el-text size="small" @click="getRefresh(directoryPath)" class="cursor-pointer">
|
||||
<el-icon>
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
<span class="sm:inline hidden pl-1">{{ $t('file.refresh') }}</span>
|
||||
</el-text>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-divider class="!my-1" />
|
||||
<el-tree-v2
|
||||
ref="treeRef"
|
||||
:data="treeData"
|
||||
:props="treeProps"
|
||||
@node-expand="handleNodeExpand"
|
||||
class="monaco-editor-tree monaco-editor-background"
|
||||
:height="treeHeight"
|
||||
:indent="6"
|
||||
:item-size="24"
|
||||
highlight-current
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<!-- 目录 -->
|
||||
<span v-if="data.isDir" style="display: inline-flex; align-items: center">
|
||||
<svg-icon className="table-icon" iconName="p-file-folder"></svg-icon>
|
||||
<small :title="node.label">{{ node.label }}</small>
|
||||
</span>
|
||||
|
||||
<!-- 文档 -->
|
||||
<span
|
||||
v-else
|
||||
style="display: inline-flex; align-items: center"
|
||||
@click="getContent(data.path, data.extension)"
|
||||
>
|
||||
<svg-icon className="table-icon" :iconName="getIconName(data.extension)"></svg-icon>
|
||||
<small :title="node.label" class="min-w-32">{{ node.label }}</small>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree-v2>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<el-divider
|
||||
v-if="isShow"
|
||||
direction="vertical"
|
||||
:style="{ height: codeHeight }"
|
||||
class="!m-0 p-0"
|
||||
:class="isShow ? 'opacity-100' : 'opacity-0'"
|
||||
></el-divider>
|
||||
<el-icon
|
||||
v-if="isShow"
|
||||
class="cursor-pointer absolute bg-gray-100 py-2 rounded-l-sm block top-1/3 -left-[9px]"
|
||||
size="9"
|
||||
@click="toggleShow"
|
||||
>
|
||||
<DArrowLeft />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
v-else
|
||||
class="cursor-pointer absolute bg-gray-100 py-2 rounded-r-sm block top-1/3 right-[7px]"
|
||||
size="9"
|
||||
@click="toggleShow"
|
||||
>
|
||||
<DArrowRight />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div
|
||||
ref="codeBox"
|
||||
id="codeBox"
|
||||
:style="{ height: codeHeight }"
|
||||
class="flex-1 sm:w-4/5 w-2/3 relative"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
@ -48,11 +139,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { SaveFileContent } from '@/api/modules/files';
|
||||
import { GetFileContent, GetFilesTree, SaveFileContent } from '@/api/modules/files';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { MsgError, MsgInfo, MsgSuccess } from '@/utils/message';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import { nextTick, onBeforeUnmount, reactive, ref } from 'vue';
|
||||
import { nextTick, onBeforeUnmount, reactive, ref, onMounted } from 'vue';
|
||||
import { Languages } from '@/global/mimetype';
|
||||
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
|
||||
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
|
||||
@ -60,6 +151,14 @@ import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
|
||||
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
|
||||
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
|
||||
|
||||
import { ElTreeV2 } from 'element-plus';
|
||||
import { ResultData } from '@/api/interface';
|
||||
import { File } from '@/api/interface/file';
|
||||
import { getIcon } from '@/utils/util';
|
||||
import { TreeKey, TreeNodeData } from 'element-plus/es/components/tree-v2/src/types';
|
||||
import { Top, Refresh, DArrowLeft, DArrowRight, FullScreen, Close } from '@element-plus/icons-vue';
|
||||
import { loadBaseDir } from '@/api/modules/setting';
|
||||
|
||||
let editor: monaco.editor.IStandaloneCodeEditor | undefined;
|
||||
|
||||
self.MonacoEnvironment = {
|
||||
@ -85,6 +184,7 @@ interface EditProps {
|
||||
content: string;
|
||||
path: string;
|
||||
name: string;
|
||||
extension: string;
|
||||
}
|
||||
|
||||
interface EditorConfig {
|
||||
@ -94,11 +194,37 @@ interface EditorConfig {
|
||||
wordWrap: WordWrapOptions;
|
||||
}
|
||||
|
||||
interface TreeNode {
|
||||
key: TreeKey;
|
||||
level: number;
|
||||
parent?: TreeNode;
|
||||
children?: File.FileTree[];
|
||||
data: TreeNodeData;
|
||||
disabled?: boolean;
|
||||
name?: string;
|
||||
isLeaf?: boolean;
|
||||
}
|
||||
|
||||
const open = ref(false);
|
||||
const loading = ref(false);
|
||||
const fileName = ref('');
|
||||
const codeThemeKey = 'code-theme';
|
||||
const warpKey = 'code-warp';
|
||||
const directoryPath = ref('');
|
||||
const fileExtension = ref('');
|
||||
const baseDir = ref();
|
||||
const treeData = ref([]);
|
||||
const codeBox = ref();
|
||||
const defaultHeight = ref(55);
|
||||
const fullScreenHeight = ref(80);
|
||||
const treeHeight = ref(0);
|
||||
const codeHeight = ref('55vh');
|
||||
const codeReq = reactive({ path: '', expand: false, page: 1, pageSize: 100 });
|
||||
const isShow = ref(true);
|
||||
|
||||
const toggleShow = () => {
|
||||
isShow.value = !isShow.value;
|
||||
};
|
||||
|
||||
type WordWrapOptions = 'off' | 'on' | 'wordWrapColumn' | 'bounded';
|
||||
|
||||
@ -155,9 +281,29 @@ const handleClose = () => {
|
||||
const loadTooltip = () => {
|
||||
return i18n.global.t('commons.button.' + (isFullscreen.value ? 'quitFullscreen' : 'fullscreen'));
|
||||
};
|
||||
function toggleFullscreen() {
|
||||
isFullscreen.value = !isFullscreen.value;
|
||||
|
||||
onMounted(() => {
|
||||
loadPath();
|
||||
updateHeights();
|
||||
window.addEventListener('resize', updateHeights);
|
||||
});
|
||||
|
||||
const updateHeights = () => {
|
||||
const vh = window.innerHeight / 100;
|
||||
if (isFullscreen.value) {
|
||||
treeHeight.value = fullScreenHeight.value * vh - 31;
|
||||
codeHeight.value = `${fullScreenHeight.value}vh`;
|
||||
} else {
|
||||
treeHeight.value = defaultHeight.value * vh - 31;
|
||||
codeHeight.value = `${defaultHeight.value}vh`;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
isFullscreen.value = !isFullscreen.value;
|
||||
updateHeights();
|
||||
};
|
||||
|
||||
const changeLanguage = () => {
|
||||
monaco.editor.setModelLanguage(editor.getModel(), config.language);
|
||||
};
|
||||
@ -183,8 +329,7 @@ const initEditor = () => {
|
||||
editor.dispose();
|
||||
}
|
||||
nextTick(() => {
|
||||
const codeBox = document.getElementById('codeBox');
|
||||
editor = monaco.editor.create(codeBox as HTMLElement, {
|
||||
editor = monaco.editor.create(codeBox.value as HTMLElement, {
|
||||
theme: config.theme,
|
||||
value: form.value.content,
|
||||
readOnly: false,
|
||||
@ -195,6 +340,10 @@ const initEditor = () => {
|
||||
overviewRulerBorder: false,
|
||||
wordWrap: config.wordWrap,
|
||||
});
|
||||
if (editor.getModel().getValue() === '') {
|
||||
let defaultContent = '\n\n\n\n';
|
||||
editor.getModel().setValue(defaultContent);
|
||||
}
|
||||
editor.onDidChangeModelContent(() => {
|
||||
if (editor) {
|
||||
form.value.content = editor.getValue();
|
||||
@ -231,38 +380,212 @@ const saveContent = (closePage: boolean) => {
|
||||
const acceptParams = (props: EditProps) => {
|
||||
form.value.content = props.content;
|
||||
form.value.path = props.path;
|
||||
config.language = props.language;
|
||||
directoryPath.value = getDirectoryPath(props.path);
|
||||
fileExtension.value = props.extension;
|
||||
fileName.value = props.name;
|
||||
config.language = props.language;
|
||||
config.eol = monaco.editor.EndOfLineSequence.LF;
|
||||
|
||||
config.theme = localStorage.getItem(codeThemeKey) || 'vs-dark';
|
||||
config.wordWrap = (localStorage.getItem(warpKey) as WordWrapOptions) || 'on';
|
||||
open.value = true;
|
||||
};
|
||||
|
||||
const getIconName = (extension: string) => getIcon(extension);
|
||||
|
||||
const loadPath = async () => {
|
||||
const pathRes = await loadBaseDir();
|
||||
baseDir.value = pathRes.data;
|
||||
};
|
||||
|
||||
const getDirectoryPath = (filePath: string) => {
|
||||
if (!filePath) {
|
||||
return baseDir.value;
|
||||
}
|
||||
|
||||
const lastSlashIndex = filePath.lastIndexOf('/');
|
||||
|
||||
if (lastSlashIndex === -1) {
|
||||
return baseDir.value;
|
||||
}
|
||||
|
||||
const directoryPath = filePath.substring(0, lastSlashIndex);
|
||||
if (directoryPath === '' || directoryPath === '.' || directoryPath === '/') {
|
||||
return baseDir.value;
|
||||
}
|
||||
return directoryPath;
|
||||
};
|
||||
|
||||
const onOpen = () => {
|
||||
initEditor();
|
||||
search(directoryPath.value).then((res) => {
|
||||
handleSearchResult(res);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSearchResult = (res: ResultData<File.FileTree[]>) => {
|
||||
if (res.data.length > 0 && res.data[0].children) {
|
||||
treeData.value = res.data[0].children.map((item) => ({
|
||||
...item,
|
||||
children: item.isDir ? item.children || [] : undefined,
|
||||
}));
|
||||
} else {
|
||||
treeData.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
const getRefresh = (path: string) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
search(path).then((res) => {
|
||||
treeData.value = res.data[0].children;
|
||||
loadedNodes.value = new Set();
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.refreshSuccess'));
|
||||
}
|
||||
};
|
||||
|
||||
const getContent = (path: string, extension: string) => {
|
||||
if (form.value.path !== path) {
|
||||
codeReq.path = path;
|
||||
codeReq.expand = true;
|
||||
|
||||
if (extension != '') {
|
||||
Languages.forEach((language) => {
|
||||
const ext = extension.substring(1);
|
||||
if (language.value.indexOf(ext) > -1) {
|
||||
config.language = language.label;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
GetFileContent(codeReq)
|
||||
.then((res) => {
|
||||
form.value.content = res.data.content;
|
||||
form.value.path = res.data.path;
|
||||
fileExtension.value = res.data.extension;
|
||||
fileName.value = res.data.name;
|
||||
initEditor();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
const initTreeData = () => ({
|
||||
path: '/',
|
||||
expand: true,
|
||||
showHidden: true,
|
||||
page: 1,
|
||||
pageSize: 1000,
|
||||
search: '',
|
||||
containSub: true,
|
||||
dir: false,
|
||||
sortBy: 'name',
|
||||
sortOrder: 'ascending',
|
||||
});
|
||||
|
||||
let req = reactive(initTreeData());
|
||||
|
||||
const loadedNodes = ref(new Set());
|
||||
|
||||
const search = async (path: string) => {
|
||||
req.path = path;
|
||||
if (req.search != '') {
|
||||
req.sortBy = 'name';
|
||||
req.sortOrder = 'ascending';
|
||||
}
|
||||
return await GetFilesTree(req);
|
||||
};
|
||||
|
||||
const getUpData = async () => {
|
||||
if ('/' === directoryPath.value) {
|
||||
MsgInfo(i18n.global.t('commons.msg.rootInfoErr'));
|
||||
return;
|
||||
}
|
||||
let pathParts = directoryPath.value.split('/');
|
||||
pathParts.pop();
|
||||
let newPath = pathParts.join('/') || '/';
|
||||
|
||||
try {
|
||||
const response = await search(newPath);
|
||||
treeData.value = response.data[0]?.children || [];
|
||||
loadedNodes.value = new Set();
|
||||
} catch (error) {
|
||||
MsgError(i18n.global.t('commons.msg.notRecords'));
|
||||
} finally {
|
||||
directoryPath.value = newPath;
|
||||
}
|
||||
};
|
||||
|
||||
const treeRef = ref<InstanceType<typeof ElTreeV2>>();
|
||||
|
||||
const treeProps = {
|
||||
value: 'id',
|
||||
label: 'name',
|
||||
children: 'children',
|
||||
};
|
||||
|
||||
const handleNodeExpand = async (node: any, data: TreeNode) => {
|
||||
if (!data.data.isDir || loadedNodes.value.has(data.data.path)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await search(node.path);
|
||||
const newTreeData = JSON.parse(JSON.stringify(treeData.value));
|
||||
if (response.data.length > 0 && response.data[0].children) {
|
||||
data.children = response.data[0].children;
|
||||
loadedNodes.value.add(data.data.path);
|
||||
updateNodeChildren(newTreeData, data.data.path, response.data[0].children);
|
||||
} else {
|
||||
data.children = [];
|
||||
}
|
||||
treeData.value = newTreeData;
|
||||
} catch (error) {
|
||||
MsgError(i18n.global.t('commons.msg.notRecords'));
|
||||
}
|
||||
};
|
||||
|
||||
// 更新指定节点的 children 数据
|
||||
const updateNodeChildren = (nodes: any[], path: any, newChildren: File.FileTree[]) => {
|
||||
const updateNode = (nodes: string | any[]) => {
|
||||
for (const element of nodes) {
|
||||
if (element.path === path) {
|
||||
element.children = newChildren;
|
||||
break;
|
||||
}
|
||||
if (element.children && element.children.length) {
|
||||
updateNode(element.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
updateNode(nodes);
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (editor) {
|
||||
editor.dispose();
|
||||
}
|
||||
window.removeEventListener('resize', updateHeights);
|
||||
});
|
||||
|
||||
defineExpose({ acceptParams });
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped lang="scss">
|
||||
.dialog-top {
|
||||
top: 0;
|
||||
}
|
||||
.fullScreen {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
position: absolute;
|
||||
right: 50px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
|
||||
.dialog-header-icon {
|
||||
color: var(--el-color-info);
|
||||
}
|
||||
|
||||
.monaco-editor-tree {
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
|
||||
.tree-widget {
|
||||
background-color: var(--el-button--primary);
|
||||
}
|
||||
</style>
|
||||
|
@ -89,7 +89,7 @@ const onConfirm = () => {
|
||||
const pros = [];
|
||||
for (const s of files.value) {
|
||||
if (s['path'].indexOf('.1panel_clash') > -1) {
|
||||
MsgWarning(i18n.global.t('file.clashDleteAlert'));
|
||||
MsgWarning(i18n.global.t('file.clashDeleteAlert'));
|
||||
return;
|
||||
}
|
||||
pros.push(DeleteFile({ path: s['path'], isDir: s['isDir'], forceDelete: forceDelete.value }));
|
||||
|
@ -386,7 +386,7 @@ let pointer = -1;
|
||||
const fileCreate = reactive({ path: '/', isDir: false, mode: 0o755 });
|
||||
const fileCompress = reactive({ files: [''], name: '', dst: '', operate: 'compress' });
|
||||
const fileDeCompress = reactive({ path: '', name: '', dst: '', mimeType: '' });
|
||||
const fileEdit = reactive({ content: '', path: '', name: '', language: 'plaintext' });
|
||||
const fileEdit = reactive({ content: '', path: '', name: '', language: 'plaintext', extension: '' });
|
||||
const codeReq = reactive({ path: '', expand: false, page: 1, pageSize: 100 });
|
||||
const fileUpload = reactive({ path: '' });
|
||||
const fileRename = reactive({ path: '', oldName: '' });
|
||||
@ -698,6 +698,7 @@ const openCodeEditor = (path: string, extension: string) => {
|
||||
fileEdit.content = res.data.content;
|
||||
fileEdit.path = res.data.path;
|
||||
fileEdit.name = res.data.name;
|
||||
fileEdit.extension = res.data.extension;
|
||||
|
||||
codeEditorRef.value.acceptParams(fileEdit);
|
||||
})
|
||||
@ -894,6 +895,10 @@ const buttons = [
|
||||
label: i18n.global.t('file.copyDir'),
|
||||
click: copyDir,
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('file.openWithVscode'),
|
||||
click: openWithVSCode,
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
disabled: (row: File.File) => {
|
||||
@ -905,10 +910,6 @@ const buttons = [
|
||||
label: i18n.global.t('file.info'),
|
||||
click: openDetail,
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('file.openWithVscode'),
|
||||
click: openWithVSCode,
|
||||
},
|
||||
];
|
||||
|
||||
onMounted(() => {
|
||||
|
Loading…
Reference in New Issue
Block a user