mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-08 22:46:51 +08:00
parent
3370bac67a
commit
3b6bb9bd22
13 changed files with 678 additions and 173 deletions
|
@ -842,3 +842,31 @@ func (b *BaseApi) GetPathByType(c *gin.Context) {
|
|||
resPath := fileService.GetPathByType(pathType)
|
||||
helper.SuccessWithData(c, resPath)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary system mount
|
||||
// @Accept json
|
||||
// @Success 200 {object} dto.DiskInfo
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /mount [post]
|
||||
func (b *BaseApi) GetHostMount(c *gin.Context) {
|
||||
disks := fileService.GetHostMount()
|
||||
helper.SuccessWithData(c, disks)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary system user and group
|
||||
// @Accept json
|
||||
// @Success 200 {object} response.UserGroupResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /user/group [post]
|
||||
func (b *BaseApi) GetUsersAndGroups(c *gin.Context) {
|
||||
res, err := fileService.GetUsersAndGroups()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, res)
|
||||
}
|
||||
|
|
|
@ -46,7 +46,8 @@ func (b *BaseApi) WsSSH(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
defer client.Close()
|
||||
sws, err := terminal.NewLogicSshWsSession(cols, rows, client.Client, wsConn, "")
|
||||
command := c.DefaultQuery("command", "")
|
||||
sws, err := terminal.NewLogicSshWsSession(cols, rows, client.Client, wsConn, command)
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -54,3 +54,13 @@ type ExistFileInfo struct {
|
|||
Size int64 `json:"size"`
|
||||
ModTime time.Time `json:"modTime"`
|
||||
}
|
||||
|
||||
type UserInfo struct {
|
||||
Username string `json:"username"`
|
||||
Group string `json:"group"`
|
||||
}
|
||||
|
||||
type UserGroupResponse struct {
|
||||
Users []UserInfo `json:"users"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
@ -55,6 +60,8 @@ type IFileService interface {
|
|||
|
||||
GetPathByType(pathType string) string
|
||||
BatchCheckFiles(req request.FilePathsCheck) []response.ExistFileInfo
|
||||
GetHostMount() []dto.DiskInfo
|
||||
GetUsersAndGroups() (*response.UserGroupResponse, error)
|
||||
}
|
||||
|
||||
var filteredPaths = []string{
|
||||
|
@ -544,3 +551,101 @@ func (f *FileService) BatchCheckFiles(req request.FilePathsCheck) []response.Exi
|
|||
}
|
||||
return fileList
|
||||
}
|
||||
|
||||
func (f *FileService) GetHostMount() []dto.DiskInfo {
|
||||
return loadDiskInfo()
|
||||
}
|
||||
|
||||
func (f *FileService) GetUsersAndGroups() (*response.UserGroupResponse, error) {
|
||||
groupMap, err := getValidGroups()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users, groupSet, err := getValidUsers(groupMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var groups []string
|
||||
for group := range groupSet {
|
||||
groups = append(groups, group)
|
||||
}
|
||||
sort.Strings(groups)
|
||||
|
||||
return &response.UserGroupResponse{
|
||||
Users: users,
|
||||
Groups: groups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getValidGroups() (map[string]bool, error) {
|
||||
groupFile, err := os.Open("/etc/group")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open /etc/group: %w", err)
|
||||
}
|
||||
defer groupFile.Close()
|
||||
|
||||
groupMap := make(map[string]bool)
|
||||
scanner := bufio.NewScanner(groupFile)
|
||||
for scanner.Scan() {
|
||||
parts := strings.Split(scanner.Text(), ":")
|
||||
if len(parts) < 3 {
|
||||
continue
|
||||
}
|
||||
groupName := parts[0]
|
||||
gid, _ := strconv.Atoi(parts[2])
|
||||
if groupName == "root" || gid >= 1000 {
|
||||
groupMap[groupName] = true
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan /etc/group: %w", err)
|
||||
}
|
||||
return groupMap, nil
|
||||
}
|
||||
|
||||
func getValidUsers(validGroups map[string]bool) ([]response.UserInfo, map[string]struct{}, error) {
|
||||
passwdFile, err := os.Open("/etc/passwd")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to open /etc/passwd: %w", err)
|
||||
}
|
||||
defer passwdFile.Close()
|
||||
|
||||
var users []response.UserInfo
|
||||
groupSet := make(map[string]struct{})
|
||||
scanner := bufio.NewScanner(passwdFile)
|
||||
for scanner.Scan() {
|
||||
parts := strings.Split(scanner.Text(), ":")
|
||||
if len(parts) < 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
username := parts[0]
|
||||
uid, _ := strconv.Atoi(parts[2])
|
||||
gid := parts[3]
|
||||
|
||||
if username != "root" && uid < 1000 {
|
||||
continue
|
||||
}
|
||||
|
||||
groupName := gid
|
||||
if g, err := user.LookupGroupId(gid); err == nil {
|
||||
groupName = g.Name
|
||||
}
|
||||
|
||||
if !validGroups[groupName] {
|
||||
continue
|
||||
}
|
||||
|
||||
users = append(users, response.UserInfo{
|
||||
Username: username,
|
||||
Group: groupName,
|
||||
})
|
||||
groupSet[groupName] = struct{}{}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to scan /etc/passwd: %w", err)
|
||||
}
|
||||
return users, groupSet, nil
|
||||
}
|
||||
|
|
|
@ -49,5 +49,7 @@ func (f *FileRouter) InitRouter(Router *gin.RouterGroup) {
|
|||
fileRouter.POST("/favorite/del", baseApi.DeleteFavorite)
|
||||
|
||||
fileRouter.GET("/path/:type", baseApi.GetPathByType)
|
||||
fileRouter.POST("/mount", baseApi.GetHostMount)
|
||||
fileRouter.POST("/user/group", baseApi.GetUsersAndGroups)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -206,4 +206,13 @@ export namespace File {
|
|||
group: string;
|
||||
sub: boolean;
|
||||
}
|
||||
|
||||
export interface UserGroupResponse {
|
||||
users: UserInfo[];
|
||||
groups: string[];
|
||||
}
|
||||
export interface UserInfo {
|
||||
username: string;
|
||||
group: string;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { AxiosRequestConfig } from 'axios';
|
|||
import { ResPage } from '../interface';
|
||||
import { TimeoutEnum } from '@/enums/http-enum';
|
||||
import { ReqPage } from '@/api/interface';
|
||||
import { Dashboard } from '@/api/interface/dashboard';
|
||||
|
||||
export const getFilesList = (params: File.ReqFile) => {
|
||||
return http.post<File.File>('files/search', params, TimeoutEnum.T_5M);
|
||||
|
@ -144,3 +145,11 @@ export const getRecycleStatusByNode = (node: string) => {
|
|||
export const getPathByType = (pathType: string) => {
|
||||
return http.get<string>(`files/path/${pathType}`);
|
||||
};
|
||||
|
||||
export const searchHostMount = () => {
|
||||
return http.post<Dashboard.DiskInfo[]>(`/files/mount`);
|
||||
};
|
||||
|
||||
export const searchUserGroup = () => {
|
||||
return http.post<File.UserGroupResponse>(`/files/user/group`);
|
||||
};
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
:page-sizes="[5, 10, 20, 50, 100]"
|
||||
@size-change="sizeChange"
|
||||
@current-change="currentChange"
|
||||
:small="mobile || paginationConfig.small"
|
||||
:size="mobile || paginationConfig.small ? 'small' : 'default'"
|
||||
:layout="mobile ? 'total, prev, pager, next' : 'total, sizes, prev, pager, next, jumper'"
|
||||
/>
|
||||
</slot>
|
||||
|
|
|
@ -109,6 +109,12 @@ const showBack = computed(() => {
|
|||
.el-button + .el-button {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.el-button-group > .el-button + .el-button {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
.el-button-group > .el-button:not(:last-child) {
|
||||
margin-right: -1px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.content-container_form {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="flex w-full flex-col gap-2 md:flex-row items-center">
|
||||
<div class="flex flex-wrap items-center" v-if="props.footer">
|
||||
<div class="flex flex-wrap gap-y-2 items-center" v-if="props.footer">
|
||||
<el-link type="primary" :underline="false" @click="toForum">
|
||||
<span class="font-normal">{{ $t('setting.forum') }}</span>
|
||||
</el-link>
|
||||
|
@ -14,31 +14,22 @@
|
|||
<span class="font-normal">{{ $t('setting.project') }}</span>
|
||||
</el-link>
|
||||
<el-divider direction="vertical" />
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center">
|
||||
<el-link :underline="false" class="-ml-2" type="primary" @click="toLxware">
|
||||
{{ $t(!isMasterProductPro ? 'license.community' : 'license.pro') }}
|
||||
</el-link>
|
||||
<el-link :underline="false" class="version" type="primary" @click="copyText(version)">
|
||||
{{ version }}
|
||||
</el-link>
|
||||
<el-badge is-dot class="-mt-0.5" v-if="version !== 'Waiting' && globalStore.hasNewVersion">
|
||||
<el-link class="ml-2" :underline="false" type="primary" @click="onLoadUpgradeInfo">
|
||||
{{ $t('commons.button.update') }}
|
||||
<div class="flex flex-wrap items-center">
|
||||
<el-link :underline="false" type="primary" @click="toLxware">
|
||||
{{ $t(!isMasterProductPro ? 'license.community' : 'license.pro') }}
|
||||
</el-link>
|
||||
</el-badge>
|
||||
<el-link
|
||||
v-if="version !== 'Waiting' && !globalStore.hasNewVersion"
|
||||
type="primary"
|
||||
:underline="false"
|
||||
class="ml-2"
|
||||
@click="onLoadUpgradeInfo"
|
||||
>
|
||||
{{ $t('commons.button.update') }}
|
||||
</el-link>
|
||||
<el-tag v-if="version === 'Waiting'" round class="ml-2.5">
|
||||
{{ $t('setting.upgrading') }}
|
||||
</el-tag>
|
||||
<el-link :underline="false" class="version" type="primary" @click="copyText(version)">
|
||||
{{ version }}
|
||||
</el-link>
|
||||
<el-badge is-dot class="-mt-0.5" :hidden="version !== 'Waiting' && globalStore.hasNewVersion">
|
||||
<el-link class="ml-2" :underline="false" type="primary" @click="onLoadUpgradeInfo">
|
||||
{{ $t('commons.button.update') }}
|
||||
</el-link>
|
||||
</el-badge>
|
||||
<el-tag v-if="version === 'Waiting'" round class="ml-2.5">
|
||||
{{ $t('setting.upgrading') }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -4,10 +4,20 @@
|
|||
<FileRole :mode="mode" @get-mode="getMode" :key="open.toString()"></FileRole>
|
||||
<el-form ref="fileForm" label-position="left" :model="addForm" label-width="100px" :rules="rules">
|
||||
<el-form-item :label="$t('commons.table.user')" prop="user">
|
||||
<el-input v-model.trim="addForm.user" />
|
||||
<el-select v-model="addForm.user" @change="handleUserChange" filterable allow-create>
|
||||
<el-option
|
||||
v-for="item in users"
|
||||
:key="item.username"
|
||||
:label="item.username"
|
||||
:value="item.username"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('file.group')" prop="group">
|
||||
<el-input v-model.trim="addForm.group" />
|
||||
<el-select v-model="addForm.group" filterable allow-create>
|
||||
<el-option v-for="group in groups" :key="group" :label="group" :value="group" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="addForm.sub">{{ $t('file.containSub') }}</el-checkbox>
|
||||
|
@ -26,7 +36,7 @@
|
|||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import { File } from '@/api/interface/file';
|
||||
import { batchChangeRole } from '@/api/modules/files';
|
||||
import { batchChangeRole, searchUserGroup } from '@/api/modules/files';
|
||||
import i18n from '@/lang';
|
||||
import FileRole from '@/components/file-role/index.vue';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
|
@ -41,6 +51,8 @@ const open = ref(false);
|
|||
const loading = ref(false);
|
||||
const mode = ref('0755');
|
||||
const files = ref<File.File[]>([]);
|
||||
const users = ref<File.UserInfo[]>([]);
|
||||
const groups = ref<string[]>([]);
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
user: [Rules.requiredInput],
|
||||
|
@ -76,6 +88,23 @@ const acceptParams = (props: BatchRoleProps) => {
|
|||
open.value = true;
|
||||
};
|
||||
|
||||
const getUserAndGroup = async () => {
|
||||
try {
|
||||
const res = await searchUserGroup();
|
||||
users.value = res.data.users;
|
||||
groups.value = res.data.groups;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user and group:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUserChange = (val: string) => {
|
||||
const found = users.value.find((u) => u.username === val);
|
||||
if (found) {
|
||||
addForm.group = found.group;
|
||||
}
|
||||
};
|
||||
|
||||
const getMode = (val: number) => {
|
||||
addForm.mode = val;
|
||||
};
|
||||
|
@ -96,6 +125,9 @@ const submit = async () => {
|
|||
loading.value = false;
|
||||
});
|
||||
};
|
||||
onMounted(() => {
|
||||
getUserAndGroup();
|
||||
});
|
||||
|
||||
defineExpose({ acceptParams });
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 flex items-center mr-4">
|
||||
<div class="flex gap-y-2 items-center gap-x-4">
|
||||
<div class="flex-shrink-0 flex items-center justify-between">
|
||||
<el-tooltip :content="$t('file.back')" placement="top">
|
||||
<el-button icon="Back" @click="back" circle />
|
||||
</el-tooltip>
|
||||
|
@ -15,39 +15,139 @@
|
|||
<el-button icon="Refresh" circle @click="search" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div
|
||||
v-show="!searchableStatus"
|
||||
tabindex="0"
|
||||
@click="searchableStatus = true"
|
||||
class="address-bar shadow-md rounded-md px-4 py-2 flex items-center flex-grow"
|
||||
>
|
||||
<div ref="pathRef" class="w-full">
|
||||
<span ref="breadCrumbRef" class="w-full flex items-center">
|
||||
<div class="flex-1 hidden sm:block" ref="pathRef">
|
||||
<div
|
||||
v-show="!searchableStatus"
|
||||
@click="searchableStatus = true"
|
||||
class="address-bar shadow-md rounded-md px-4 py-2 flex items-center flex-grow"
|
||||
>
|
||||
<div ref="breadCrumbRef" class="flex items-center address-url">
|
||||
<span class="root mr-2">
|
||||
<el-link @click.stop="jump('/')">
|
||||
<el-icon :size="20"><HomeFilled /></el-icon>
|
||||
</el-link>
|
||||
</span>
|
||||
<span v-for="path in paths" :key="path.url" class="inline-flex items-center">
|
||||
<span v-for="(path, index) in paths" :key="path.url" class="inline-flex items-center">
|
||||
<span class="mr-2 arrow">></span>
|
||||
<el-link class="path-segment cursor-pointer mr-2 pathname" @click.stop="jump(path.url)">
|
||||
{{ path.name }}
|
||||
<template v-if="index === 0 && hidePaths.length > 0">
|
||||
<el-dropdown>
|
||||
<span
|
||||
class="path-segment cursor-pointer mr-2 pathname focus:outline-none focus-visible:outline-none"
|
||||
>
|
||||
..
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="hidePath in hidePaths"
|
||||
:key="hidePath.url"
|
||||
@click.stop="jump(hidePath.url)"
|
||||
>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
:content="hidePath.name"
|
||||
placement="bottom"
|
||||
>
|
||||
{{
|
||||
hidePath.name.length > 25
|
||||
? hidePath.name.substring(0, 22) + '...'
|
||||
: hidePath.name
|
||||
}}
|
||||
</el-tooltip>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<span class="mr-2 arrow">></span>
|
||||
<el-tooltip class="box-item" effect="dark" :content="path.name" placement="bottom">
|
||||
<el-link
|
||||
class="path-segment cursor-pointer mr-2 pathname"
|
||||
@click.stop="jump(path.url)"
|
||||
>
|
||||
{{ path.name.length > 25 ? path.name.substring(0, 22) + '...' : path.name }}
|
||||
</el-link>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-tooltip class="box-item" effect="dark" :content="path.name" placement="bottom">
|
||||
<el-link
|
||||
class="path-segment cursor-pointer mr-2 pathname"
|
||||
@click.stop="jump(path.url)"
|
||||
>
|
||||
{{ path.name.length > 25 ? path.name.substring(0, 22) + '...' : path.name }}
|
||||
</el-link>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-input
|
||||
ref="searchableInputRef"
|
||||
v-show="searchableStatus"
|
||||
v-model="searchablePath"
|
||||
@blur="searchableInputBlur"
|
||||
class="px-4 py-2 border rounded-md shadow-md"
|
||||
@keyup.enter="
|
||||
jump(searchablePath);
|
||||
searchableStatus = false;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1 sm:hidden block">
|
||||
<div class="address-bar shadow-md rounded-md px-4 py-2 flex items-center flex-grow">
|
||||
<div class="flex items-center address-url">
|
||||
<span class="root mr-2">
|
||||
<el-link @click.stop="jump('/')">
|
||||
<el-icon :size="20"><HomeFilled /></el-icon>
|
||||
</el-link>
|
||||
</span>
|
||||
</span>
|
||||
<span v-for="(path, index) in paths" :key="path.url" class="inline-flex items-center">
|
||||
<span class="mr-2 arrow">></span>
|
||||
<template v-if="index === 0 && hidePaths.length > 0">
|
||||
<el-dropdown>
|
||||
<span
|
||||
class="path-segment cursor-pointer mr-2 pathname focus:outline-none focus-visible:outline-none"
|
||||
>
|
||||
..
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="hidePath in hidePaths"
|
||||
:key="hidePath.url"
|
||||
@click.stop="jump(hidePath.url)"
|
||||
>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
:content="hidePath.name"
|
||||
placement="bottom"
|
||||
>
|
||||
{{
|
||||
hidePath.name.length > 25
|
||||
? hidePath.name.substring(0, 22) + '...'
|
||||
: hidePath.name
|
||||
}}
|
||||
</el-tooltip>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<span class="mr-2 arrow">></span>
|
||||
<el-tooltip class="box-item" effect="dark" :content="path.name" placement="bottom">
|
||||
<el-link
|
||||
class="path-segment cursor-pointer mr-2 pathname"
|
||||
@click.stop="jump(path.url)"
|
||||
>
|
||||
{{ path.name.length > 25 ? path.name.substring(0, 22) + '...' : path.name }}
|
||||
</el-link>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-input
|
||||
ref="searchableInputRef"
|
||||
v-show="searchableStatus"
|
||||
v-model="searchablePath"
|
||||
@blur="searchableInputBlur"
|
||||
class="px-4 py-2 border rounded-md shadow-md"
|
||||
@keyup.enter="
|
||||
jump(searchablePath);
|
||||
searchableStatus = false;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<LayoutContent :title="$t('menu.files')" v-loading="loading">
|
||||
<template #prompt>
|
||||
|
@ -60,115 +160,187 @@
|
|||
</el-alert>
|
||||
</template>
|
||||
<template #leftToolBar>
|
||||
<el-dropdown @command="handleCreate" class="mr-2.5">
|
||||
<el-button type="primary">
|
||||
{{ $t('commons.button.create') }}
|
||||
<el-icon><arrow-down /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="dir">
|
||||
<svg-icon iconName="p-file-folder"></svg-icon>
|
||||
{{ $t('file.dir') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="file">
|
||||
<svg-icon iconName="p-file-normal"></svg-icon>
|
||||
{{ $t('menu.files') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button-group>
|
||||
<el-button plain @click="openUpload">{{ $t('commons.button.upload') }}</el-button>
|
||||
<el-button plain @click="openWget">{{ $t('file.remoteFile') }}</el-button>
|
||||
<el-button plain @click="openMove('copy')" :disabled="selects.length === 0">
|
||||
{{ $t('commons.button.copy') }}
|
||||
</el-button>
|
||||
<el-button plain @click="openMove('cut')" :disabled="selects.length === 0">
|
||||
{{ $t('file.move') }}
|
||||
</el-button>
|
||||
<el-button plain @click="openCompress(selects)" :disabled="selects.length === 0">
|
||||
{{ $t('file.compress') }}
|
||||
</el-button>
|
||||
<el-button plain @click="openBatchRole(selects)" :disabled="selects.length === 0">
|
||||
{{ $t('file.role') }}
|
||||
</el-button>
|
||||
<el-button plain @click="batchDelFiles" :disabled="selects.length === 0">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
<div ref="leftWrapper">
|
||||
<el-dropdown @command="handleCreate" class="mr-2.5">
|
||||
<el-button type="primary">
|
||||
{{ $t('commons.button.create') }}
|
||||
<el-icon><arrow-down /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="dir">
|
||||
<svg-icon iconName="p-file-folder"></svg-icon>
|
||||
{{ $t('file.dir') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="file">
|
||||
<svg-icon iconName="p-file-normal"></svg-icon>
|
||||
{{ $t('menu.files') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button-group>
|
||||
<el-button plain @click="openUpload">{{ $t('commons.button.upload') }}</el-button>
|
||||
<el-button plain @click="openWget">{{ $t('file.remoteFile') }}</el-button>
|
||||
<el-button class="btn mr-2.5" @click="openRecycleBin">
|
||||
{{ $t('file.recycleBin') }}
|
||||
</el-button>
|
||||
<el-button class="btn" @click="toTerminal">
|
||||
{{ $t('menu.terminal') }}
|
||||
</el-button>
|
||||
<el-popover placement="bottom" :width="200" trigger="hover" @before-enter="getFavoriates">
|
||||
<template #reference>
|
||||
<el-button @click="openFavorite">
|
||||
{{ $t('file.favorite') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<div class="favorite-item">
|
||||
<el-table :data="favorites">
|
||||
<el-table-column prop="name">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
:content="row.path"
|
||||
placement="top"
|
||||
>
|
||||
<span
|
||||
class="table-link text-ellipsis"
|
||||
@click="toFavorite(row)"
|
||||
type="primary"
|
||||
>
|
||||
<svg-icon
|
||||
v-if="row.isDir"
|
||||
className="table-icon"
|
||||
iconName="p-file-folder"
|
||||
></svg-icon>
|
||||
<svg-icon
|
||||
v-else
|
||||
className="table-icon"
|
||||
iconName="p-file-normal"
|
||||
></svg-icon>
|
||||
{{ row.name }}
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-popover>
|
||||
<template v-if="hostMount.length == 1">
|
||||
<el-button class="btn" @click.stop="jump(hostMount[0]?.path)">
|
||||
{{ hostMount[0]?.path }} ({{ $t('file.root') }}) {{ getFileSize(hostMount[0]?.free) }}
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-dropdown class="mr-2.5" style="border-left: 1px solid #ccc">
|
||||
<el-button class="btn">
|
||||
{{ hostMount[0]?.path }} ({{ $t('file.root') }})
|
||||
{{ getFileSize(hostMount[0]?.free) }}
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<template v-for="(mount, index) in hostMount" :key="mount.path">
|
||||
<el-dropdown-item v-if="index != 0" @click.stop="jump(mount.path)">
|
||||
{{ mount.path }} ({{ $t('home.mount') }}) {{ getFileSize(mount.free) }}
|
||||
</el-dropdown-item>
|
||||
</template>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-button-group>
|
||||
|
||||
<el-button class="btn" @click="toTerminal">
|
||||
{{ $t('menu.terminal') }}
|
||||
</el-button>
|
||||
|
||||
<el-badge :value="processCount" class="btn" v-if="processCount > 0">
|
||||
<el-button class="btn" @click="openProcess">
|
||||
{{ $t('file.wgetTask') }}
|
||||
</el-button>
|
||||
</el-badge>
|
||||
|
||||
<el-button-group class="copy-button" v-if="moveOpen">
|
||||
<el-tooltip class="box-item" effect="dark" :content="$t('file.paste')" placement="bottom">
|
||||
<el-button plain @click="openPaste">{{ $t('file.paste') }}({{ fileMove.count }})</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
:content="$t('commons.button.cancel')"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-button plain class="close" icon="Close" @click="closeMove"></el-button>
|
||||
</el-tooltip>
|
||||
</el-button-group>
|
||||
<el-badge :value="processCount" class="btn" v-if="processCount > 0">
|
||||
<el-button class="btn" @click="openProcess">
|
||||
{{ $t('file.wgetTask') }}
|
||||
</el-button>
|
||||
</el-badge>
|
||||
</div>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
<el-popover placement="bottom" :width="200" trigger="hover" @before-enter="getFavoriates">
|
||||
<template #reference>
|
||||
<el-button @click="openFavorite">
|
||||
{{ $t('file.favorite') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<div class="favorite-item">
|
||||
<el-table :data="favorites">
|
||||
<el-table-column prop="name">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip class="box-item" effect="dark" :content="row.path" placement="top">
|
||||
<span class="table-link text-ellipsis" @click="toFavorite(row)" type="primary">
|
||||
<svg-icon
|
||||
v-if="row.isDir"
|
||||
className="table-icon"
|
||||
iconName="p-file-folder"
|
||||
></svg-icon>
|
||||
<svg-icon v-else className="table-icon" iconName="p-file-normal"></svg-icon>
|
||||
{{ row.name }}
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<div ref="btnWrapper" class="flex items-center gap-2 flex-wrap">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<template v-if="visibleButtons.length == 0">
|
||||
<el-dropdown v-if="moreButtons.length">
|
||||
<el-button>
|
||||
{{ $t('tabs.more') }}
|
||||
<i class="el-icon-arrow-down el-icon--right" />
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="btn in moreButtons"
|
||||
:key="btn.label"
|
||||
@click="btn.action"
|
||||
:disabled="selects.length === 0"
|
||||
>
|
||||
{{ $t(btn.label) }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
<template v-if="visibleButtons.length > 0">
|
||||
<el-button-group class="flex items-center">
|
||||
<template v-for="btn in visibleButtons" :key="btn.label">
|
||||
<el-button plain @click="btn.action" :disabled="selects.length === 0">
|
||||
{{ $t(btn.label) }}
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<el-dropdown v-if="moreButtons.length" style="border-left: 1px solid #ccc">
|
||||
<el-button>
|
||||
{{ $t('tabs.more') }}
|
||||
<i class="el-icon-arrow-down el-icon--right" />
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="btn in moreButtons"
|
||||
:key="btn.label"
|
||||
@click="btn.action"
|
||||
:disabled="selects.length === 0"
|
||||
>
|
||||
{{ $t(btn.label) }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-button-group>
|
||||
</template>
|
||||
</div>
|
||||
<el-button-group class="copy-button" v-if="moveOpen">
|
||||
<el-tooltip class="box-item" effect="dark" :content="$t('file.paste')" placement="bottom">
|
||||
<el-button plain @click="openPaste">{{ $t('file.paste') }}({{ fileMove.count }})</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
:content="$t('commons.button.cancel')"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-button plain class="close" icon="Close" @click="closeMove"></el-button>
|
||||
</el-tooltip>
|
||||
</el-button-group>
|
||||
<div class="w-80">
|
||||
<el-input
|
||||
v-model="req.search"
|
||||
clearable
|
||||
@clear="search()"
|
||||
@keydown.enter="search()"
|
||||
:placeholder="$t('file.search')"
|
||||
>
|
||||
<template #prepend>
|
||||
<el-checkbox v-model="req.containSub">
|
||||
{{ $t('file.sub') }}
|
||||
</el-checkbox>
|
||||
</template>
|
||||
<template #append>
|
||||
<el-button icon="Search" @click="search" round />
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</el-popover>
|
||||
<el-button class="btn mr-2.5" @click="openRecycleBin">
|
||||
{{ $t('file.recycleBin') }}
|
||||
</el-button>
|
||||
<div class="w-76">
|
||||
<el-input
|
||||
v-model="req.search"
|
||||
clearable
|
||||
@clear="search()"
|
||||
@keydown.enter="search()"
|
||||
:placeholder="$t('file.search')"
|
||||
>
|
||||
<template #prepend>
|
||||
<el-checkbox v-model="req.containSub">
|
||||
{{ $t('file.sub') }}
|
||||
</el-checkbox>
|
||||
</template>
|
||||
<template #append>
|
||||
<el-button icon="Search" @click="search" round />
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</template>
|
||||
<template #main>
|
||||
|
@ -306,6 +478,7 @@
|
|||
<BatchRole ref="batchRoleRef" @close="search" />
|
||||
<VscodeOpenDialog ref="dialogVscodeOpenRef" />
|
||||
<Preview ref="previewRef" />
|
||||
<TerminalDialog ref="dialogTerminalRef" />
|
||||
</LayoutContent>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -320,6 +493,7 @@ import {
|
|||
removeFavorite,
|
||||
searchFavorite,
|
||||
fileWgetKeys,
|
||||
searchHostMount,
|
||||
} from '@/api/modules/files';
|
||||
import { computeSize, copyText, dateFormat, getFileType, getIcon, getRandomStr, downloadFile } from '@/utils/util';
|
||||
import { File } from '@/api/interface/file';
|
||||
|
@ -350,6 +524,9 @@ import Favorite from './favorite/index.vue';
|
|||
import BatchRole from './batch-role/index.vue';
|
||||
import Preview from './preview/index.vue';
|
||||
import VscodeOpenDialog from '@/components/vscode-open/index.vue';
|
||||
import { debounce } from 'lodash-es';
|
||||
import TerminalDialog from './terminal/index.vue';
|
||||
import { Dashboard } from '@/api/interface/dashboard';
|
||||
|
||||
const globalStore = GlobalStore();
|
||||
|
||||
|
@ -378,6 +555,7 @@ const initData = () => ({
|
|||
let req = reactive(initData());
|
||||
let loading = ref(false);
|
||||
const paths = ref<FilePaths[]>([]);
|
||||
const hidePaths = ref<FilePaths[]>([]);
|
||||
let pathWidth = ref(0);
|
||||
const history: string[] = [];
|
||||
let pointer = -1;
|
||||
|
@ -417,6 +595,8 @@ const batchRoleRef = ref();
|
|||
const dialogVscodeOpenRef = ref();
|
||||
const previewRef = ref();
|
||||
const processRef = ref();
|
||||
const hostMount = ref<Dashboard.DiskInfo[]>([]);
|
||||
let resizeObserver: ResizeObserver;
|
||||
|
||||
const { searchableStatus, searchablePath, searchableInputRef, searchableInputBlur } = useSearchable(paths);
|
||||
|
||||
|
@ -494,23 +674,91 @@ const copyDir = (row: File.File) => {
|
|||
}
|
||||
};
|
||||
|
||||
const handlePath = () => {
|
||||
const breadcrumbElement = breadCrumbRef.value as any;
|
||||
const pathNodes = breadcrumbElement.querySelectorAll('.pathname');
|
||||
const btnWrapper = ref<HTMLElement | null>(null);
|
||||
const leftWrapper = ref<HTMLElement | null>(null);
|
||||
const toolButtons = ref([
|
||||
{
|
||||
label: 'commons.button.copy',
|
||||
action: () => openMove('copy'),
|
||||
},
|
||||
{
|
||||
label: 'file.move',
|
||||
action: () => openMove('cut'),
|
||||
},
|
||||
{
|
||||
label: 'file.compress',
|
||||
action: () => openCompress(selects.value),
|
||||
},
|
||||
{
|
||||
label: 'file.role',
|
||||
action: () => openBatchRole(selects.value),
|
||||
},
|
||||
{
|
||||
label: 'commons.button.delete',
|
||||
action: () => batchDelFiles(),
|
||||
},
|
||||
]);
|
||||
|
||||
let totalpathWidth = 0;
|
||||
pathNodes.forEach((node) => {
|
||||
totalpathWidth += node.offsetWidth;
|
||||
});
|
||||
if (totalpathWidth > pathWidth.value) {
|
||||
paths.value.splice(0, 1);
|
||||
paths.value[0].name = '..';
|
||||
nextTick(function () {
|
||||
handlePath();
|
||||
});
|
||||
const visibleButtons = ref([...toolButtons.value]);
|
||||
const moreButtons = ref([]);
|
||||
|
||||
const updateButtons = async () => {
|
||||
await nextTick();
|
||||
if (!btnWrapper.value) return;
|
||||
const pathWidth = pathRef.value.offsetWidth;
|
||||
const leftWidth = leftWrapper.value.offsetWidth;
|
||||
let num = Math.floor((pathWidth - leftWidth - 420) / 70);
|
||||
if (num < 0) {
|
||||
visibleButtons.value = toolButtons.value;
|
||||
moreButtons.value = [];
|
||||
} else {
|
||||
visibleButtons.value = toolButtons.value.slice(0, num);
|
||||
moreButtons.value = toolButtons.value.slice(num);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePath = () => {
|
||||
nextTick(function () {
|
||||
let breadCrumbWidth = breadCrumbRef.value.offsetWidth;
|
||||
let pathWidth = pathRef.value.offsetWidth;
|
||||
if (pathWidth - breadCrumbWidth < 50 && paths.value.length > 1) {
|
||||
const removed = paths.value.shift();
|
||||
if (removed) hidePaths.value.push(removed);
|
||||
handlePath();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const resizeHandler = debounce(() => {
|
||||
resetPaths();
|
||||
handlePath();
|
||||
}, 100);
|
||||
|
||||
const btnResizeHandler = debounce(() => {
|
||||
updateButtons();
|
||||
}, 100);
|
||||
|
||||
const observeResize = () => {
|
||||
const el = pathRef.value as any;
|
||||
if (!el) return;
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
resizeHandler();
|
||||
});
|
||||
|
||||
const ele = btnWrapper.value as any;
|
||||
if (!ele) return;
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
btnResizeHandler();
|
||||
});
|
||||
resizeObserver.observe(el);
|
||||
resizeObserver.observe(ele);
|
||||
};
|
||||
|
||||
const resetPaths = () => {
|
||||
paths.value = [...hidePaths.value, ...paths.value];
|
||||
hidePaths.value = [];
|
||||
};
|
||||
|
||||
const right = () => {
|
||||
if (pointer < history.length - 1) {
|
||||
pointer++;
|
||||
|
@ -578,6 +826,7 @@ const backForwardJump = async (url: string) => {
|
|||
const getPaths = (reqPath: string) => {
|
||||
const pathArray = reqPath.split('/');
|
||||
paths.value = [];
|
||||
hidePaths.value = [];
|
||||
let base = '/';
|
||||
for (const p of pathArray) {
|
||||
if (p != '') {
|
||||
|
@ -895,8 +1144,9 @@ const toFavorite = (row: File.Favorite) => {
|
|||
}
|
||||
};
|
||||
|
||||
const dialogTerminalRef = ref();
|
||||
const toTerminal = () => {
|
||||
router.push({ path: '/terminal', query: { path: req.path } });
|
||||
dialogTerminalRef.value!.acceptParams({ cwd: req.path, command: '/bin/sh' });
|
||||
};
|
||||
|
||||
const openWithVSCode = (row: File.File) => {
|
||||
|
@ -975,7 +1225,17 @@ const isDecompressFile = (row: File.File) => {
|
|||
}
|
||||
};
|
||||
|
||||
const getHostMount = async () => {
|
||||
try {
|
||||
const res = await searchHostMount();
|
||||
hostMount.value = res.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching host mount:', error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getHostMount();
|
||||
if (router.currentRoute.value.query.path) {
|
||||
req.path = String(router.currentRoute.value.query.path);
|
||||
getPaths(req.path);
|
||||
|
@ -992,8 +1252,13 @@ onMounted(() => {
|
|||
pointer = history.length - 1;
|
||||
nextTick(function () {
|
||||
handlePath();
|
||||
observeResize();
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (resizeObserver) resizeObserver.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
|
47
frontend/src/views/host/file-management/terminal/index.vue
Normal file
47
frontend/src/views/host/file-management/terminal/index.vue
Normal file
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<DrawerPro v-model="terminalVisible" :header="$t('menu.terminal')" @close="handleClose" size="large">
|
||||
<template #content>
|
||||
<Terminal style="height: calc(100vh - 100px)" ref="terminalRef"></Terminal>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, nextTick } from 'vue';
|
||||
import Terminal from '@/components/terminal/index.vue';
|
||||
|
||||
const terminalVisible = ref(false);
|
||||
const terminalRef = ref<InstanceType<typeof Terminal> | null>(null);
|
||||
|
||||
interface DialogProps {
|
||||
cwd: string;
|
||||
command: string;
|
||||
}
|
||||
const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
terminalVisible.value = true;
|
||||
await initTerm(params.cwd);
|
||||
};
|
||||
|
||||
const initTerm = async (cwd: string) => {
|
||||
await nextTick();
|
||||
terminalRef.value!.acceptParams({
|
||||
endpoint: '/api/v2/hosts/terminal',
|
||||
args: `command=${encodeURIComponent(`clear && cd ${cwd}`)}`,
|
||||
error: '',
|
||||
initCmd: '',
|
||||
});
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
terminalRef.value?.onClose();
|
||||
};
|
||||
|
||||
function handleClose() {
|
||||
onClose();
|
||||
terminalVisible.value = false;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
Loading…
Add table
Reference in a new issue