mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-09-11 00:54:55 +08:00
feat: 文件夹树形列表实现
This commit is contained in:
parent
ba8a13dfcc
commit
cc8d6516d9
8 changed files with 121 additions and 73 deletions
|
@ -20,3 +20,17 @@ func (b *BaseApi) ListFiles(c *gin.Context) {
|
||||||
}
|
}
|
||||||
helper.SuccessWithData(c, files)
|
helper.SuccessWithData(c, files)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) GetFileTree(c *gin.Context) {
|
||||||
|
var req dto.FileOption
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tree, err := fileService.GetFileTree(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, tree)
|
||||||
|
}
|
||||||
|
|
|
@ -9,3 +9,9 @@ type FileOption struct {
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
files.FileInfo
|
files.FileInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FileTree struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Children []FileTree `json:"children"`
|
||||||
|
}
|
||||||
|
|
|
@ -25,3 +25,24 @@ func (f FileService) GetFileList(op dto.FileOption) (dto.FileInfo, error) {
|
||||||
fileInfo.FileInfo = *info
|
fileInfo.FileInfo = *info
|
||||||
return fileInfo, nil
|
return fileInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f FileService) GetFileTree(op dto.FileOption) ([]dto.FileTree, error) {
|
||||||
|
var treeArray []dto.FileTree
|
||||||
|
info, err := files.NewFileInfo(op.FileOption)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
node := dto.FileTree{
|
||||||
|
Name: info.Name,
|
||||||
|
Path: info.Path,
|
||||||
|
}
|
||||||
|
for _, v := range info.Items {
|
||||||
|
if v.IsDir {
|
||||||
|
node.Children = append(node.Children, dto.FileTree{
|
||||||
|
Name: v.Name,
|
||||||
|
Path: v.Path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return append(treeArray, node), nil
|
||||||
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ func (f *FileRouter) InitFileRouter(Router *gin.RouterGroup) {
|
||||||
baseApi := v1.ApiGroupApp.BaseApi
|
baseApi := v1.ApiGroupApp.BaseApi
|
||||||
{
|
{
|
||||||
fileRouter.POST("/search", baseApi.ListFiles)
|
fileRouter.POST("/search", baseApi.ListFiles)
|
||||||
|
fileRouter.POST("/tree", baseApi.GetFileTree)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
|
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||||
import { showFullScreenLoading, tryHideFullScreenLoading } from '@/config/service-loading';
|
// import { showFullScreenLoading, tryHideFullScreenLoading } from '@/config/service-loading';
|
||||||
import { AxiosCanceler } from './helper/axios-cancel';
|
import { AxiosCanceler } from './helper/axios-cancel';
|
||||||
import { ResultData } from '@/api/interface';
|
import { ResultData } from '@/api/interface';
|
||||||
import { ResultEnum } from '@/enums/http-enum';
|
import { ResultEnum } from '@/enums/http-enum';
|
||||||
|
@ -31,7 +31,7 @@ class RequestHttp {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
axiosCanceler.addPending(config);
|
axiosCanceler.addPending(config);
|
||||||
config.headers!.noLoading || showFullScreenLoading();
|
// config.headers!.noLoading || showFullScreenLoading();
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
};
|
};
|
||||||
|
@ -48,7 +48,7 @@ class RequestHttp {
|
||||||
globalStore.setCsrfToken(response.headers['x-csrf-token']);
|
globalStore.setCsrfToken(response.headers['x-csrf-token']);
|
||||||
}
|
}
|
||||||
axiosCanceler.removePending(config);
|
axiosCanceler.removePending(config);
|
||||||
tryHideFullScreenLoading();
|
// tryHideFullScreenLoading();
|
||||||
if (data.code == ResultEnum.OVERDUE || data.code == ResultEnum.FORBIDDEN) {
|
if (data.code == ResultEnum.OVERDUE || data.code == ResultEnum.FORBIDDEN) {
|
||||||
ElMessage.error(data.msg);
|
ElMessage.error(data.msg);
|
||||||
router.replace({
|
router.replace({
|
||||||
|
@ -64,7 +64,7 @@ class RequestHttp {
|
||||||
},
|
},
|
||||||
async (error: AxiosError) => {
|
async (error: AxiosError) => {
|
||||||
const { response } = error;
|
const { response } = error;
|
||||||
tryHideFullScreenLoading();
|
// tryHideFullScreenLoading();
|
||||||
if (error.message.indexOf('timeout') !== -1) ElMessage.error('请求超时!请您稍后重试');
|
if (error.message.indexOf('timeout') !== -1) ElMessage.error('请求超时!请您稍后重试');
|
||||||
if (response) checkStatus(response.status);
|
if (response) checkStatus(response.status);
|
||||||
if (!window.navigator.onLine) router.replace({ path: '/500' });
|
if (!window.navigator.onLine) router.replace({ path: '/500' });
|
||||||
|
|
|
@ -21,4 +21,11 @@ export namespace File {
|
||||||
search?: string;
|
search?: string;
|
||||||
expand: boolean;
|
expand: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FileTree {
|
||||||
|
name: string;
|
||||||
|
isDir: Boolean;
|
||||||
|
path: string;
|
||||||
|
children?: FileTree[];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,3 +4,7 @@ import http from '@/api';
|
||||||
export const GetFilesList = (params: File.ReqFile) => {
|
export const GetFilesList = (params: File.ReqFile) => {
|
||||||
return http.post<File.File>('files/search', params);
|
return http.post<File.File>('files/search', params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const GetFilesTree = (params: File.ReqFile) => {
|
||||||
|
return http.post<File.FileTree[]>('files/tree', params);
|
||||||
|
};
|
||||||
|
|
|
@ -1,19 +1,28 @@
|
||||||
<template>
|
<template>
|
||||||
<LayoutContent :header="$t('menu.files')">
|
<LayoutContent :header="$t('menu.files')">
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="6">
|
<el-col :span="5">
|
||||||
<el-tree :data="dataSource" node-key="id">
|
<el-scrollbar height="800px">
|
||||||
<template #default="{ node }">
|
<el-tree
|
||||||
<el-icon v-if="node.data.isDir && node.expanded"><FolderOpened /></el-icon>
|
:data="fileTree"
|
||||||
<el-icon v-if="node.data.isDir && !node.expanded"><Folder /></el-icon>
|
:props="defaultProps"
|
||||||
<el-icon v-if="!node.data.isDir"><Document /></el-icon>
|
:load="loadNode"
|
||||||
<span class="custom-tree-node">
|
lazy
|
||||||
<span>{{ node.data.label }}</span>
|
node-key="id"
|
||||||
</span>
|
v-loading="treeLoading"
|
||||||
</template>
|
>
|
||||||
</el-tree>
|
<template #default="{ node }">
|
||||||
|
<el-icon v-if="node.expanded"><FolderOpened /></el-icon>
|
||||||
|
<el-icon v-else><Folder /></el-icon>
|
||||||
|
<span class="custom-tree-node">
|
||||||
|
<span>{{ node.data.name }}</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
</el-scrollbar>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="18">
|
|
||||||
|
<el-col :span="19">
|
||||||
<div class="path">
|
<div class="path">
|
||||||
<BreadCrumbs>
|
<BreadCrumbs>
|
||||||
<BreadCrumbItem @click="jump(-1)" :right="paths.length == 0">root</BreadCrumbItem>
|
<BreadCrumbItem @click="jump(-1)" :right="paths.length == 0">root</BreadCrumbItem>
|
||||||
|
@ -30,7 +39,7 @@
|
||||||
:pagination-config="paginationConfig"
|
:pagination-config="paginationConfig"
|
||||||
v-model:selects="selects"
|
v-model:selects="selects"
|
||||||
:data="data"
|
:data="data"
|
||||||
:loading="loading"
|
v-loading="loading"
|
||||||
>
|
>
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<el-dropdown split-button type="primary">
|
<el-dropdown split-button type="primary">
|
||||||
|
@ -89,26 +98,29 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, reactive, ref } from '@vue/runtime-core';
|
import { reactive, ref } from '@vue/runtime-core';
|
||||||
import LayoutContent from '@/layout/layout-content.vue';
|
import LayoutContent from '@/layout/layout-content.vue';
|
||||||
import ComplexTable from '@/components/complex-table/index.vue';
|
import ComplexTable from '@/components/complex-table/index.vue';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import { GetFilesList } from '@/api/modules/files';
|
import { GetFilesList, GetFilesTree } from '@/api/modules/files';
|
||||||
import { dateFromat } from '@/utils/util';
|
import { dateFromat } from '@/utils/util';
|
||||||
import { File } from '@/api/interface/file';
|
import { File } from '@/api/interface/file';
|
||||||
import BreadCrumbs from '@/components/bread-crumbs/index.vue';
|
import BreadCrumbs from '@/components/bread-crumbs/index.vue';
|
||||||
import BreadCrumbItem from '@/components/bread-crumbs/bread-crumbs-item.vue';
|
import BreadCrumbItem from '@/components/bread-crumbs/bread-crumbs-item.vue';
|
||||||
interface Tree {
|
|
||||||
id: number;
|
|
||||||
label: string;
|
|
||||||
isDir: Boolean;
|
|
||||||
children?: Tree[];
|
|
||||||
}
|
|
||||||
let data = ref();
|
let data = ref();
|
||||||
let selects = ref<any>([]);
|
let selects = ref<any>([]);
|
||||||
let req = reactive({ path: '/', expand: true });
|
let req = reactive({ path: '/', expand: true });
|
||||||
let loading = ref<boolean>(false);
|
let loading = ref<boolean>(false);
|
||||||
|
let treeLoading = ref<boolean>(false);
|
||||||
let paths = ref<string[]>([]);
|
let paths = ref<string[]>([]);
|
||||||
|
let fileTree = ref<File.FileTree[]>([]);
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
children: 'children',
|
||||||
|
label: 'name',
|
||||||
|
};
|
||||||
|
|
||||||
const paginationConfig = reactive({
|
const paginationConfig = reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 5,
|
pageSize: 5,
|
||||||
|
@ -135,9 +147,9 @@ const buttons = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const search = (req: File.ReqFile) => {
|
const search = async (req: File.ReqFile) => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
GetFilesList(req)
|
await GetFilesList(req)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
data.value = res.data.items;
|
data.value = res.data.items;
|
||||||
req.path = res.data.path;
|
req.path = res.data.path;
|
||||||
|
@ -176,53 +188,36 @@ const jump = async (index: number) => {
|
||||||
search(req);
|
search(req);
|
||||||
};
|
};
|
||||||
|
|
||||||
const dataSource = ref<Tree[]>([
|
const getTree = async (req: File.ReqFile, node: File.FileTree | null) => {
|
||||||
{
|
treeLoading.value = true;
|
||||||
id: 1,
|
await GetFilesTree(req)
|
||||||
label: 'var',
|
.then((res) => {
|
||||||
isDir: true,
|
if (node) {
|
||||||
children: [
|
if (res.data.length > 0) {
|
||||||
{
|
node.children = res.data[0].children;
|
||||||
id: 4,
|
}
|
||||||
label: 'log',
|
} else {
|
||||||
isDir: true,
|
fileTree.value = res.data;
|
||||||
children: [
|
}
|
||||||
{
|
search(req);
|
||||||
id: 9,
|
})
|
||||||
isDir: false,
|
.finally(() => {
|
||||||
label: 'ko.log',
|
treeLoading.value = false;
|
||||||
},
|
});
|
||||||
{
|
};
|
||||||
id: 10,
|
|
||||||
isDir: false,
|
|
||||||
label: 'kubepi.log',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
label: 'opt',
|
|
||||||
isDir: true,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
isDir: false,
|
|
||||||
label: 'app.conf',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
isDir: false,
|
|
||||||
label: 'test.txt',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
onMounted(() => {
|
const loadNode = (node: any, resolve: (data: File.FileTree[]) => void) => {
|
||||||
search(req);
|
console.log(node.id);
|
||||||
});
|
if (!node.hasChildNodes) {
|
||||||
|
if (node.data.path) {
|
||||||
|
req.path = node.data.path;
|
||||||
|
getTree(req, node.data);
|
||||||
|
} else {
|
||||||
|
getTree(req, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve([]);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
Loading…
Add table
Reference in a new issue