mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-09 15:06:37 +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)
|
resPath := fileService.GetPathByType(pathType)
|
||||||
helper.SuccessWithData(c, resPath)
|
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
|
return
|
||||||
}
|
}
|
||||||
defer client.Close()
|
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) {
|
if wshandleError(wsConn, err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,3 +54,13 @@ type ExistFileInfo struct {
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
ModTime time.Time `json:"modTime"`
|
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
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
|
"os/user"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
@ -55,6 +60,8 @@ type IFileService interface {
|
||||||
|
|
||||||
GetPathByType(pathType string) string
|
GetPathByType(pathType string) string
|
||||||
BatchCheckFiles(req request.FilePathsCheck) []response.ExistFileInfo
|
BatchCheckFiles(req request.FilePathsCheck) []response.ExistFileInfo
|
||||||
|
GetHostMount() []dto.DiskInfo
|
||||||
|
GetUsersAndGroups() (*response.UserGroupResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var filteredPaths = []string{
|
var filteredPaths = []string{
|
||||||
|
@ -544,3 +551,101 @@ func (f *FileService) BatchCheckFiles(req request.FilePathsCheck) []response.Exi
|
||||||
}
|
}
|
||||||
return fileList
|
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.POST("/favorite/del", baseApi.DeleteFavorite)
|
||||||
|
|
||||||
fileRouter.GET("/path/:type", baseApi.GetPathByType)
|
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;
|
group: string;
|
||||||
sub: boolean;
|
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 { ResPage } from '../interface';
|
||||||
import { TimeoutEnum } from '@/enums/http-enum';
|
import { TimeoutEnum } from '@/enums/http-enum';
|
||||||
import { ReqPage } from '@/api/interface';
|
import { ReqPage } from '@/api/interface';
|
||||||
|
import { Dashboard } from '@/api/interface/dashboard';
|
||||||
|
|
||||||
export const getFilesList = (params: File.ReqFile) => {
|
export const getFilesList = (params: File.ReqFile) => {
|
||||||
return http.post<File.File>('files/search', params, TimeoutEnum.T_5M);
|
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) => {
|
export const getPathByType = (pathType: string) => {
|
||||||
return http.get<string>(`files/path/${pathType}`);
|
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]"
|
:page-sizes="[5, 10, 20, 50, 100]"
|
||||||
@size-change="sizeChange"
|
@size-change="sizeChange"
|
||||||
@current-change="currentChange"
|
@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'"
|
:layout="mobile ? 'total, prev, pager, next' : 'total, sizes, prev, pager, next, jumper'"
|
||||||
/>
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
|
|
|
@ -109,6 +109,12 @@ const showBack = computed(() => {
|
||||||
.el-button + .el-button {
|
.el-button + .el-button {
|
||||||
margin: 0 !important;
|
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 {
|
.content-container_form {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex w-full flex-col gap-2 md:flex-row items-center">
|
<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">
|
<el-link type="primary" :underline="false" @click="toForum">
|
||||||
<span class="font-normal">{{ $t('setting.forum') }}</span>
|
<span class="font-normal">{{ $t('setting.forum') }}</span>
|
||||||
</el-link>
|
</el-link>
|
||||||
|
@ -14,31 +14,22 @@
|
||||||
<span class="font-normal">{{ $t('setting.project') }}</span>
|
<span class="font-normal">{{ $t('setting.project') }}</span>
|
||||||
</el-link>
|
</el-link>
|
||||||
<el-divider direction="vertical" />
|
<el-divider direction="vertical" />
|
||||||
</div>
|
<div class="flex flex-wrap items-center">
|
||||||
<div class="flex flex-wrap items-center">
|
<el-link :underline="false" type="primary" @click="toLxware">
|
||||||
<el-link :underline="false" class="-ml-2" type="primary" @click="toLxware">
|
{{ $t(!isMasterProductPro ? 'license.community' : 'license.pro') }}
|
||||||
{{ $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') }}
|
|
||||||
</el-link>
|
</el-link>
|
||||||
</el-badge>
|
<el-link :underline="false" class="version" type="primary" @click="copyText(version)">
|
||||||
<el-link
|
{{ version }}
|
||||||
v-if="version !== 'Waiting' && !globalStore.hasNewVersion"
|
</el-link>
|
||||||
type="primary"
|
<el-badge is-dot class="-mt-0.5" :hidden="version !== 'Waiting' && globalStore.hasNewVersion">
|
||||||
:underline="false"
|
<el-link class="ml-2" :underline="false" type="primary" @click="onLoadUpgradeInfo">
|
||||||
class="ml-2"
|
{{ $t('commons.button.update') }}
|
||||||
@click="onLoadUpgradeInfo"
|
</el-link>
|
||||||
>
|
</el-badge>
|
||||||
{{ $t('commons.button.update') }}
|
<el-tag v-if="version === 'Waiting'" round class="ml-2.5">
|
||||||
</el-link>
|
{{ $t('setting.upgrading') }}
|
||||||
<el-tag v-if="version === 'Waiting'" round class="ml-2.5">
|
</el-tag>
|
||||||
{{ $t('setting.upgrading') }}
|
</div>
|
||||||
</el-tag>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,20 @@
|
||||||
<FileRole :mode="mode" @get-mode="getMode" :key="open.toString()"></FileRole>
|
<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 ref="fileForm" label-position="left" :model="addForm" label-width="100px" :rules="rules">
|
||||||
<el-form-item :label="$t('commons.table.user')" prop="user">
|
<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>
|
||||||
|
|
||||||
<el-form-item :label="$t('file.group')" prop="group">
|
<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-form-item>
|
<el-form-item>
|
||||||
<el-checkbox v-model="addForm.sub">{{ $t('file.containSub') }}</el-checkbox>
|
<el-checkbox v-model="addForm.sub">{{ $t('file.containSub') }}</el-checkbox>
|
||||||
|
@ -26,7 +36,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref } from 'vue';
|
import { reactive, ref } from 'vue';
|
||||||
import { File } from '@/api/interface/file';
|
import { File } from '@/api/interface/file';
|
||||||
import { batchChangeRole } from '@/api/modules/files';
|
import { batchChangeRole, searchUserGroup } from '@/api/modules/files';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import FileRole from '@/components/file-role/index.vue';
|
import FileRole from '@/components/file-role/index.vue';
|
||||||
import { MsgSuccess } from '@/utils/message';
|
import { MsgSuccess } from '@/utils/message';
|
||||||
|
@ -41,6 +51,8 @@ const open = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const mode = ref('0755');
|
const mode = ref('0755');
|
||||||
const files = ref<File.File[]>([]);
|
const files = ref<File.File[]>([]);
|
||||||
|
const users = ref<File.UserInfo[]>([]);
|
||||||
|
const groups = ref<string[]>([]);
|
||||||
|
|
||||||
const rules = reactive<FormRules>({
|
const rules = reactive<FormRules>({
|
||||||
user: [Rules.requiredInput],
|
user: [Rules.requiredInput],
|
||||||
|
@ -76,6 +88,23 @@ const acceptParams = (props: BatchRoleProps) => {
|
||||||
open.value = true;
|
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) => {
|
const getMode = (val: number) => {
|
||||||
addForm.mode = val;
|
addForm.mode = val;
|
||||||
};
|
};
|
||||||
|
@ -96,6 +125,9 @@ const submit = async () => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
onMounted(() => {
|
||||||
|
getUserAndGroup();
|
||||||
|
});
|
||||||
|
|
||||||
defineExpose({ acceptParams });
|
defineExpose({ acceptParams });
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center">
|
<div class="flex gap-y-2 items-center gap-x-4">
|
||||||
<div class="flex-shrink-0 flex items-center mr-4">
|
<div class="flex-shrink-0 flex items-center justify-between">
|
||||||
<el-tooltip :content="$t('file.back')" placement="top">
|
<el-tooltip :content="$t('file.back')" placement="top">
|
||||||
<el-button icon="Back" @click="back" circle />
|
<el-button icon="Back" @click="back" circle />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
|
@ -15,39 +15,139 @@
|
||||||
<el-button icon="Refresh" circle @click="search" />
|
<el-button icon="Refresh" circle @click="search" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="flex-1 hidden sm:block" ref="pathRef">
|
||||||
v-show="!searchableStatus"
|
<div
|
||||||
tabindex="0"
|
v-show="!searchableStatus"
|
||||||
@click="searchableStatus = true"
|
@click="searchableStatus = true"
|
||||||
class="address-bar shadow-md rounded-md px-4 py-2 flex items-center flex-grow"
|
class="address-bar shadow-md rounded-md px-4 py-2 flex items-center flex-grow"
|
||||||
>
|
>
|
||||||
<div ref="pathRef" class="w-full">
|
<div ref="breadCrumbRef" class="flex items-center address-url">
|
||||||
<span ref="breadCrumbRef" class="w-full flex items-center">
|
|
||||||
<span class="root mr-2">
|
<span class="root mr-2">
|
||||||
<el-link @click.stop="jump('/')">
|
<el-link @click.stop="jump('/')">
|
||||||
<el-icon :size="20"><HomeFilled /></el-icon>
|
<el-icon :size="20"><HomeFilled /></el-icon>
|
||||||
</el-link>
|
</el-link>
|
||||||
</span>
|
</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>
|
<span class="mr-2 arrow">></span>
|
||||||
<el-link class="path-segment cursor-pointer mr-2 pathname" @click.stop="jump(path.url)">
|
<template v-if="index === 0 && hidePaths.length > 0">
|
||||||
{{ path.name }}
|
<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>
|
</el-link>
|
||||||
</span>
|
</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>
|
||||||
</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>
|
||||||
<LayoutContent :title="$t('menu.files')" v-loading="loading">
|
<LayoutContent :title="$t('menu.files')" v-loading="loading">
|
||||||
<template #prompt>
|
<template #prompt>
|
||||||
|
@ -60,115 +160,187 @@
|
||||||
</el-alert>
|
</el-alert>
|
||||||
</template>
|
</template>
|
||||||
<template #leftToolBar>
|
<template #leftToolBar>
|
||||||
<el-dropdown @command="handleCreate" class="mr-2.5">
|
<div ref="leftWrapper">
|
||||||
<el-button type="primary">
|
<el-dropdown @command="handleCreate" class="mr-2.5">
|
||||||
{{ $t('commons.button.create') }}
|
<el-button type="primary">
|
||||||
<el-icon><arrow-down /></el-icon>
|
{{ $t('commons.button.create') }}
|
||||||
</el-button>
|
<el-icon><arrow-down /></el-icon>
|
||||||
<template #dropdown>
|
</el-button>
|
||||||
<el-dropdown-menu>
|
<template #dropdown>
|
||||||
<el-dropdown-item command="dir">
|
<el-dropdown-menu>
|
||||||
<svg-icon iconName="p-file-folder"></svg-icon>
|
<el-dropdown-item command="dir">
|
||||||
{{ $t('file.dir') }}
|
<svg-icon iconName="p-file-folder"></svg-icon>
|
||||||
</el-dropdown-item>
|
{{ $t('file.dir') }}
|
||||||
<el-dropdown-item command="file">
|
</el-dropdown-item>
|
||||||
<svg-icon iconName="p-file-normal"></svg-icon>
|
<el-dropdown-item command="file">
|
||||||
{{ $t('menu.files') }}
|
<svg-icon iconName="p-file-normal"></svg-icon>
|
||||||
</el-dropdown-item>
|
{{ $t('menu.files') }}
|
||||||
</el-dropdown-menu>
|
</el-dropdown-item>
|
||||||
</template>
|
</el-dropdown-menu>
|
||||||
</el-dropdown>
|
</template>
|
||||||
<el-button-group>
|
</el-dropdown>
|
||||||
<el-button plain @click="openUpload">{{ $t('commons.button.upload') }}</el-button>
|
<el-button-group>
|
||||||
<el-button plain @click="openWget">{{ $t('file.remoteFile') }}</el-button>
|
<el-button plain @click="openUpload">{{ $t('commons.button.upload') }}</el-button>
|
||||||
<el-button plain @click="openMove('copy')" :disabled="selects.length === 0">
|
<el-button plain @click="openWget">{{ $t('file.remoteFile') }}</el-button>
|
||||||
{{ $t('commons.button.copy') }}
|
<el-button class="btn mr-2.5" @click="openRecycleBin">
|
||||||
</el-button>
|
{{ $t('file.recycleBin') }}
|
||||||
<el-button plain @click="openMove('cut')" :disabled="selects.length === 0">
|
</el-button>
|
||||||
{{ $t('file.move') }}
|
<el-button class="btn" @click="toTerminal">
|
||||||
</el-button>
|
{{ $t('menu.terminal') }}
|
||||||
<el-button plain @click="openCompress(selects)" :disabled="selects.length === 0">
|
</el-button>
|
||||||
{{ $t('file.compress') }}
|
<el-popover placement="bottom" :width="200" trigger="hover" @before-enter="getFavoriates">
|
||||||
</el-button>
|
<template #reference>
|
||||||
<el-button plain @click="openBatchRole(selects)" :disabled="selects.length === 0">
|
<el-button @click="openFavorite">
|
||||||
{{ $t('file.role') }}
|
{{ $t('file.favorite') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button plain @click="batchDelFiles" :disabled="selects.length === 0">
|
</template>
|
||||||
{{ $t('commons.button.delete') }}
|
<div class="favorite-item">
|
||||||
</el-button>
|
<el-table :data="favorites">
|
||||||
</el-button-group>
|
<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">
|
<el-badge :value="processCount" class="btn" v-if="processCount > 0">
|
||||||
{{ $t('menu.terminal') }}
|
<el-button class="btn" @click="openProcess">
|
||||||
</el-button>
|
{{ $t('file.wgetTask') }}
|
||||||
|
</el-button>
|
||||||
<el-badge :value="processCount" class="btn" v-if="processCount > 0">
|
</el-badge>
|
||||||
<el-button class="btn" @click="openProcess">
|
</div>
|
||||||
{{ $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>
|
|
||||||
</template>
|
</template>
|
||||||
<template #rightToolBar>
|
<template #rightToolBar>
|
||||||
<el-popover placement="bottom" :width="200" trigger="hover" @before-enter="getFavoriates">
|
<div ref="btnWrapper" class="flex items-center gap-2 flex-wrap">
|
||||||
<template #reference>
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
<el-button @click="openFavorite">
|
<template v-if="visibleButtons.length == 0">
|
||||||
{{ $t('file.favorite') }}
|
<el-dropdown v-if="moreButtons.length">
|
||||||
</el-button>
|
<el-button>
|
||||||
</template>
|
{{ $t('tabs.more') }}
|
||||||
<div class="favorite-item">
|
<i class="el-icon-arrow-down el-icon--right" />
|
||||||
<el-table :data="favorites">
|
</el-button>
|
||||||
<el-table-column prop="name">
|
<template #dropdown>
|
||||||
<template #default="{ row }">
|
<el-dropdown-menu>
|
||||||
<el-tooltip class="box-item" effect="dark" :content="row.path" placement="top">
|
<el-dropdown-item
|
||||||
<span class="table-link text-ellipsis" @click="toFavorite(row)" type="primary">
|
v-for="btn in moreButtons"
|
||||||
<svg-icon
|
:key="btn.label"
|
||||||
v-if="row.isDir"
|
@click="btn.action"
|
||||||
className="table-icon"
|
:disabled="selects.length === 0"
|
||||||
iconName="p-file-folder"
|
>
|
||||||
></svg-icon>
|
{{ $t(btn.label) }}
|
||||||
<svg-icon v-else className="table-icon" iconName="p-file-normal"></svg-icon>
|
</el-dropdown-item>
|
||||||
{{ row.name }}
|
</el-dropdown-menu>
|
||||||
</span>
|
|
||||||
</el-tooltip>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-dropdown>
|
||||||
</el-table>
|
</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>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #main>
|
<template #main>
|
||||||
|
@ -306,6 +478,7 @@
|
||||||
<BatchRole ref="batchRoleRef" @close="search" />
|
<BatchRole ref="batchRoleRef" @close="search" />
|
||||||
<VscodeOpenDialog ref="dialogVscodeOpenRef" />
|
<VscodeOpenDialog ref="dialogVscodeOpenRef" />
|
||||||
<Preview ref="previewRef" />
|
<Preview ref="previewRef" />
|
||||||
|
<TerminalDialog ref="dialogTerminalRef" />
|
||||||
</LayoutContent>
|
</LayoutContent>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -320,6 +493,7 @@ import {
|
||||||
removeFavorite,
|
removeFavorite,
|
||||||
searchFavorite,
|
searchFavorite,
|
||||||
fileWgetKeys,
|
fileWgetKeys,
|
||||||
|
searchHostMount,
|
||||||
} from '@/api/modules/files';
|
} from '@/api/modules/files';
|
||||||
import { computeSize, copyText, dateFormat, getFileType, getIcon, getRandomStr, downloadFile } from '@/utils/util';
|
import { computeSize, copyText, dateFormat, getFileType, getIcon, getRandomStr, downloadFile } from '@/utils/util';
|
||||||
import { File } from '@/api/interface/file';
|
import { File } from '@/api/interface/file';
|
||||||
|
@ -350,6 +524,9 @@ import Favorite from './favorite/index.vue';
|
||||||
import BatchRole from './batch-role/index.vue';
|
import BatchRole from './batch-role/index.vue';
|
||||||
import Preview from './preview/index.vue';
|
import Preview from './preview/index.vue';
|
||||||
import VscodeOpenDialog from '@/components/vscode-open/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();
|
const globalStore = GlobalStore();
|
||||||
|
|
||||||
|
@ -378,6 +555,7 @@ const initData = () => ({
|
||||||
let req = reactive(initData());
|
let req = reactive(initData());
|
||||||
let loading = ref(false);
|
let loading = ref(false);
|
||||||
const paths = ref<FilePaths[]>([]);
|
const paths = ref<FilePaths[]>([]);
|
||||||
|
const hidePaths = ref<FilePaths[]>([]);
|
||||||
let pathWidth = ref(0);
|
let pathWidth = ref(0);
|
||||||
const history: string[] = [];
|
const history: string[] = [];
|
||||||
let pointer = -1;
|
let pointer = -1;
|
||||||
|
@ -417,6 +595,8 @@ const batchRoleRef = ref();
|
||||||
const dialogVscodeOpenRef = ref();
|
const dialogVscodeOpenRef = ref();
|
||||||
const previewRef = ref();
|
const previewRef = ref();
|
||||||
const processRef = ref();
|
const processRef = ref();
|
||||||
|
const hostMount = ref<Dashboard.DiskInfo[]>([]);
|
||||||
|
let resizeObserver: ResizeObserver;
|
||||||
|
|
||||||
const { searchableStatus, searchablePath, searchableInputRef, searchableInputBlur } = useSearchable(paths);
|
const { searchableStatus, searchablePath, searchableInputRef, searchableInputBlur } = useSearchable(paths);
|
||||||
|
|
||||||
|
@ -494,23 +674,91 @@ const copyDir = (row: File.File) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePath = () => {
|
const btnWrapper = ref<HTMLElement | null>(null);
|
||||||
const breadcrumbElement = breadCrumbRef.value as any;
|
const leftWrapper = ref<HTMLElement | null>(null);
|
||||||
const pathNodes = breadcrumbElement.querySelectorAll('.pathname');
|
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;
|
const visibleButtons = ref([...toolButtons.value]);
|
||||||
pathNodes.forEach((node) => {
|
const moreButtons = ref([]);
|
||||||
totalpathWidth += node.offsetWidth;
|
|
||||||
});
|
const updateButtons = async () => {
|
||||||
if (totalpathWidth > pathWidth.value) {
|
await nextTick();
|
||||||
paths.value.splice(0, 1);
|
if (!btnWrapper.value) return;
|
||||||
paths.value[0].name = '..';
|
const pathWidth = pathRef.value.offsetWidth;
|
||||||
nextTick(function () {
|
const leftWidth = leftWrapper.value.offsetWidth;
|
||||||
handlePath();
|
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 = () => {
|
const right = () => {
|
||||||
if (pointer < history.length - 1) {
|
if (pointer < history.length - 1) {
|
||||||
pointer++;
|
pointer++;
|
||||||
|
@ -578,6 +826,7 @@ const backForwardJump = async (url: string) => {
|
||||||
const getPaths = (reqPath: string) => {
|
const getPaths = (reqPath: string) => {
|
||||||
const pathArray = reqPath.split('/');
|
const pathArray = reqPath.split('/');
|
||||||
paths.value = [];
|
paths.value = [];
|
||||||
|
hidePaths.value = [];
|
||||||
let base = '/';
|
let base = '/';
|
||||||
for (const p of pathArray) {
|
for (const p of pathArray) {
|
||||||
if (p != '') {
|
if (p != '') {
|
||||||
|
@ -895,8 +1144,9 @@ const toFavorite = (row: File.Favorite) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dialogTerminalRef = ref();
|
||||||
const toTerminal = () => {
|
const toTerminal = () => {
|
||||||
router.push({ path: '/terminal', query: { path: req.path } });
|
dialogTerminalRef.value!.acceptParams({ cwd: req.path, command: '/bin/sh' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const openWithVSCode = (row: File.File) => {
|
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(() => {
|
onMounted(() => {
|
||||||
|
getHostMount();
|
||||||
if (router.currentRoute.value.query.path) {
|
if (router.currentRoute.value.query.path) {
|
||||||
req.path = String(router.currentRoute.value.query.path);
|
req.path = String(router.currentRoute.value.query.path);
|
||||||
getPaths(req.path);
|
getPaths(req.path);
|
||||||
|
@ -992,8 +1252,13 @@ onMounted(() => {
|
||||||
pointer = history.length - 1;
|
pointer = history.length - 1;
|
||||||
nextTick(function () {
|
nextTick(function () {
|
||||||
handlePath();
|
handlePath();
|
||||||
|
observeResize();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (resizeObserver) resizeObserver.disconnect();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<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