🔨 sftp 文件列表.

This commit is contained in:
lijiahang
2024-02-10 00:05:27 +08:00
parent 8f8858fda0
commit c723eb4ac3
7 changed files with 243 additions and 68 deletions

View File

@@ -26,7 +26,7 @@ export function readFileText(e: File, encoding = 'UTF-8'): Promise<string> {
/**
* 解析路径类型
*/
type PathAnalysis = {
export interface PathAnalysis {
name: string;
path: string;
}
@@ -34,23 +34,19 @@ type PathAnalysis = {
/**
* 获取解析路径
*/
export function getPathAnalysis(analysisPath: string, paths: PathAnalysis[] = []): PathAnalysis[] {
const lastSymbol = analysisPath.lastIndexOf('/');
if (lastSymbol === -1) {
paths.unshift({
name: '/',
path: '/'
});
export function getPathAnalysis(path: string, paths: PathAnalysis[] = []): PathAnalysis[] {
const lastSeparatorIndex = path.lastIndexOf('/');
if (lastSeparatorIndex === -1) {
return paths;
}
const name = analysisPath.substring(lastSymbol, analysisPath.length);
const name = path.substring(lastSeparatorIndex, path.length);
if (!isEmptyStr(name) && name !== '/') {
paths.unshift({
name: name.substring(1, name.length),
path: analysisPath
path: path
});
}
return getPathAnalysis(analysisPath.substring(0, lastSymbol), paths);
return getPathAnalysis(path.substring(0, lastSeparatorIndex), paths);
}
/**
@@ -58,7 +54,7 @@ export function getPathAnalysis(analysisPath: string, paths: PathAnalysis[] = []
*/
export function getPath(path: string) {
return path.replace(new RegExp('\\\\+', 'g'), '/')
.replace(new RegExp('/+', 'g'), '/');
.replace(new RegExp('/+', 'g'), '/');
}
/**

View File

@@ -11,27 +11,68 @@
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="返回上级">
<span class="click-icon-wrapper header-action-icon mr4">
<span class="click-icon-wrapper header-action-icon mr4"
@click="backParentPath">
<icon-left />
</span>
</a-tooltip>
<!-- 当前路径 -->
<div class="sftp-path-wrapper">
<a-breadcrumb>
<!-- 分隔符 -->
<template #separator>
<icon-right />
</template>
<a-breadcrumb-item class="sftp-path-unit">/</a-breadcrumb-item>
<a-breadcrumb-item class="sftp-path-unit">root</a-breadcrumb-item>
<a-breadcrumb-item class="sftp-path-unit">orion</a-breadcrumb-item>
<a-breadcrumb-item class="sftp-path-unit">space</a-breadcrumb-item>
<a-breadcrumb-item class="sftp-path-unit">logs</a-breadcrumb-item>
<div class="sftp-path-wrapper"
@click="setPathEditable(true)">
<!-- 路径输入框 -->
<div v-if="pathEditable">
<a-input v-model="pathInput"
placeholder="文件夹路径"
allow-clear
@press-enter="doChangePath" />
</div>
<!-- 路径视图 -->
<a-breadcrumb v-else>
<!-- 根目录 -->
<a-breadcrumb-item class="sftp-path-unit"
@click.stop="loadFileList('/')">
<icon-home />
</a-breadcrumb-item>
<!-- 子目录 -->
<a-breadcrumb-item class="sftp-path-unit"
v-for="path in analysisPaths"
@click.stop="loadFileList(path.path)">
{{ path.name }}
</a-breadcrumb-item>
</a-breadcrumb>
</div>
</div>
<!-- 右侧操作 -->
<a-space class="sftp-table-header-right">
<!-- 路径编辑模式-右侧操作 -->
<a-space v-if="pathEditable" class="sftp-table-header-right">
<!-- 进入 -->
<a-tooltip position="top"
:mini="true"
:overlay-inverse="true"
:auto-fix-position="false"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="进入">
<span class="click-icon-wrapper header-action-icon"
@click="doChangePath">
<icon-right />
</span>
</a-tooltip>
<!-- 取消 -->
<a-tooltip position="top"
:mini="true"
:overlay-inverse="true"
:auto-fix-position="false"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="取消">
<span class="click-icon-wrapper header-action-icon"
@click="setPathEditable(false)">
<icon-stop />
</span>
</a-tooltip>
</a-space>
<!-- 非路径编辑模式-右侧操作 -->
<a-space v-else class="sftp-table-header-right">
<!-- 刷新 -->
<a-tooltip position="top"
:mini="true"
@@ -40,7 +81,8 @@
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="刷新">
<span class="click-icon-wrapper header-action-icon">
<span class="click-icon-wrapper header-action-icon"
@click="loadFileList">
<icon-refresh />
</span>
</a-tooltip>
@@ -51,9 +93,11 @@
:auto-fix-position="false"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="显示隐藏文件">
<span class="click-icon-wrapper header-action-icon">
<icon-eye />
:content="showHiddenFile ? '不显示隐藏文件' : '显示隐藏文件'">
<span class="click-icon-wrapper header-action-icon"
@click="toggleShowHiddenFile">
<icon-eye-invisible v-if="showHiddenFile" />
<icon-eye v-else />
</span>
</a-tooltip>
<!-- 创建文件 -->
@@ -64,7 +108,8 @@
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="创建文件">
<span class="click-icon-wrapper header-action-icon">
<span class="click-icon-wrapper header-action-icon"
@click="createFile">
<icon-drive-file />
</span>
</a-tooltip>
@@ -76,23 +121,25 @@
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="创建文件夹">
<span class="click-icon-wrapper header-action-icon">
<span class="click-icon-wrapper header-action-icon"
@click="createDir">
<icon-folder-add />
</span>
</a-tooltip>
<!-- 删除 -->
<!-- 删除选中文件 -->
<a-tooltip position="top"
:mini="true"
:overlay-inverse="true"
:auto-fix-position="false"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="删除">
<span class="click-icon-wrapper header-action-icon">
content="删除选中文件">
<span class="click-icon-wrapper header-action-icon"
@click="deleteSelectFiles">
<icon-delete />
</span>
</a-tooltip>
<!-- 复制 -->
<!-- 复制 FIXME 不行就删除 -->
<a-tooltip position="top"
:mini="true"
:overlay-inverse="true"
@@ -100,11 +147,11 @@
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="复制">
<span class="click-icon-wrapper header-action-icon">
<span v-if="false" class="click-icon-wrapper header-action-icon">
<icon-copy />
</span>
</a-tooltip>
<!-- 移动 -->
<!-- 移动 FIXME 不行就删除 -->
<a-tooltip position="top"
:mini="true"
:overlay-inverse="true"
@@ -124,7 +171,8 @@
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="上传">
<span class="click-icon-wrapper header-action-icon">
<span class="click-icon-wrapper header-action-icon"
@click="uploadFile">
<icon-upload />
</span>
</a-tooltip>
@@ -136,7 +184,8 @@
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="下载">
<span class="click-icon-wrapper header-action-icon">
<span class="click-icon-wrapper header-action-icon"
@click="downloadFile">
<icon-download />
</span>
</a-tooltip>
@@ -151,6 +200,91 @@
</script>
<script lang="ts" setup>
import type { PathAnalysis } from '@/utils/file';
import type { ISftpSession } from '../../types/terminal.type';
import { ref, watch } from 'vue';
import { getParentPath, getPathAnalysis } from '@/utils/file';
const props = defineProps<{
currentPath: string;
session: ISftpSession | undefined,
selectedFiles: Array<string>
}>();
const emits = defineEmits(['loadFile']);
const showHiddenFile = ref(false);
const analysisPaths = ref<Array<PathAnalysis>>([]);
const pathEditable = ref(false);
const pathInput = ref('');
// 监听路径变化
watch(() => props.currentPath, (path) => {
if (path) {
analysisPaths.value = getPathAnalysis(path);
} else {
analysisPaths.value = [];
}
});
// 返回上级目录
const backParentPath = () => {
loadFileList(getParentPath(props.currentPath));
};
// 设置命令编辑模式
const setPathEditable = (editable: boolean) => {
pathEditable.value = editable;
pathInput.value = editable ? props.currentPath : '';
};
// 执行修改目录
const doChangePath = () => {
loadFileList(pathInput.value);
setPathEditable(false);
};
// 加载文件列表
const loadFileList = (path: string = props.currentPath) => {
emits('loadFile', path);
};
// 设置是否显示隐藏文件
const toggleShowHiddenFile = () => {
showHiddenFile.value = !showHiddenFile.value;
// 设置显示状态并且刷新
if (props.session) {
props.session.setShowHiddenFile(showHiddenFile.value);
loadFileList();
}
};
// 创建文件
const createFile = () => {
// openModal(true, "props.currentPath")
};
// 创建文件夹
const createDir = () => {
// openModal(false, "props.currentPath")
};
// 删除选中文件
const deleteSelectFiles = () => {
// confirm
};
// 上传文件
const uploadFile = () => {
// openModal("props.currentPath")
};
// 下载文件
const downloadFile = () => {
};
// FIXME 图标宽度提成变量
</script>

View File

@@ -1,5 +1,5 @@
<template>
<a-table row-key="name"
<a-table row-key="path"
class="sftp-table"
label-align="left"
:columns="columns"
@@ -44,7 +44,7 @@
<!-- 修改时间/操作 -->
<template #modifyTime="{ record }">
<!-- 修改时间 -->
<span v-if="editName !== record.name">{{ dateFormat(new Date(record.modifyTime)) }}</span>
<span v-if="editRowName !== record.name">{{ dateFormat(new Date(record.modifyTime)) }}</span>
<!-- 操作 -->
<a-space v-else>
<!-- 复制路径 -->
@@ -67,10 +67,10 @@
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="删除">
<span class="click-icon-wrapper row-action-icon"
@click="deleteFile(record.path)">
<icon-delete />
</span>
<span class="click-icon-wrapper row-action-icon"
@click="deleteFile(record.path)">
<icon-delete />
</span>
</a-tooltip>
<!-- 下载 -->
<a-tooltip position="top"
@@ -79,10 +79,10 @@
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="下载">
<span class="click-icon-wrapper row-action-icon"
@click="downloadFile(record.path)">
<icon-download />
</span>
<span class="click-icon-wrapper row-action-icon"
@click="downloadFile(record.path)">
<icon-download />
</span>
</a-tooltip>
<!-- 移动 -->
<a-tooltip position="top"
@@ -91,10 +91,10 @@
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="移动">
<span class="click-icon-wrapper row-action-icon"
@click="moveFile(record.path)">
<icon-paste />
</span>
<span class="click-icon-wrapper row-action-icon"
@click="moveFile(record.path)">
<icon-paste />
</span>
</a-tooltip>
<!-- 提权 -->
<a-tooltip position="top"
@@ -103,10 +103,10 @@
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="提权">
<span class="click-icon-wrapper row-action-icon"
@click="chmodFile(record.path)">
<icon-user-group />
</span>
<span class="click-icon-wrapper row-action-icon"
@click="chmodFile(record.path, record.attr)">
<icon-user-group />
</span>
</a-tooltip>
</a-space>
</template>
@@ -129,20 +129,37 @@
import useCopy from '@/hooks/copy';
import { FILE_TYPE } from '../../types/terminal.const';
import type { ISftpSession } from '../../types/terminal.type';
import useLoading from '@/hooks/loading';
import { useCacheStore } from '@/store';
import { computed } from 'vue/dist/vue';
const props = defineProps<{
session: ISftpSession | undefined;
list: Array<SftpFile>;
loading: boolean;
selectedFiles: Array<string>;
}>();
const emits = defineEmits(['update:selectedFiles']);
const rowSelection = useRowSelection({ width: 40 });
const { copy } = useCopy();
const selectedKeys = ref<Array<string>>([]);
const editName = ref<string>('');
const selectedKeys = computed({
get() {
return props.selectedFiles;
},
set(e) {
emits('update:selectedFiles', e);
}
});
const editRowName = ref<string>('');
// 设置选中状态
const setEditable = (record: TableData) => {
editName.value = record.name;
editRowName.value = record.name;
record.hover = true;
};
@@ -150,8 +167,8 @@
const unsetEditable = (record: TableData) => {
setTimeout(() => {
// 等待后如果还是当前行 但是未被选中则代表已经被失焦
if (record.name === editName.value && !record.hover) {
editName.value = '';
if (record.name === editRowName.value && !record.hover) {
editRowName.value = '';
}
}, 20);
record.hover = false;
@@ -159,6 +176,7 @@
// 删除文件
const deleteFile = (path: string) => {
// confirm
};
// 下载文件
@@ -167,10 +185,13 @@
// 移动文件
const moveFile = (path: string) => {
// openModal('path')
};
// 文件提权
const chmodFile = (path: string) => {
const chmodFile = (path: string, attr: string) => {
// openModal('path','mod')
};
// 格式化文件类型

View File

@@ -6,11 +6,18 @@
:disabled="!editView">
<!-- 左侧面板表格 -->
<template #first>
<!-- FIXME spin -->
<div class="sftp-table-container">
<!-- 表头 -->
<sftp-table-header class="sftp-table-header" />
<sftp-table-header class="sftp-table-header"
:current-path="currentPath"
:session="session"
:selected-files="selectFiles"
@load-file="loadFiles" />
<!-- 表格 -->
<sftp-table class="sftp-table-wrapper"
v-model:selected-files="selectFiles"
:session="session"
:list="fileList"
:loading="tableLoading" />
</div>
@@ -48,19 +55,25 @@
const session = ref<ISftpSession>();
const currentPath = ref<string>('');
const fileList = ref<Array<SftpFile>>(mockData);
const selectFiles = ref<Array<string>>([]);
const splitSize = ref(1);
const editView = ref(true);
// 连接成功回调
const connectCallback = () => {
loadFiles(undefined);
};
// 加载文件列表
const loadFiles = (path: string | undefined) => {
setTableLoading(true);
session.value?.list(undefined);
session.value?.list(path);
};
// 接收列表回调
const resolveList = (result: string, path: string, list: Array<SftpFile>) => {
const success = !!Number.parseInt(result);
setLoading(false);
setTableLoading(false);
if (!success) {
Message.error('查询失败');
return;

View File

@@ -12,6 +12,8 @@ export default class SftpSession implements ISftpSession {
public resolver: ISftpSessionResolver;
private showHiddenFile: boolean;
private readonly channel: ITerminalChannel;
constructor(hostId: number,
@@ -21,6 +23,7 @@ export default class SftpSession implements ISftpSession {
this.sessionId = sessionId;
this.channel = channel;
this.connected = false;
this.showHiddenFile = false;
this.resolver = undefined as unknown as ISftpSessionResolver;
}
@@ -36,10 +39,16 @@ export default class SftpSession implements ISftpSession {
this.resolver.connectCallback();
}
// 设置显示隐藏文件
setShowHiddenFile(show: boolean): void {
this.showHiddenFile = show;
}
// 查询文件列表
list(path: string | undefined) {
this.channel.send(InputProtocol.SFTP_LIST, {
sessionId: this.sessionId,
showHiddenFile: ~~this.showHiddenFile,
path
});
};

View File

@@ -33,7 +33,7 @@ export const InputProtocol = {
// SFTP 文件列表
SFTP_LIST: {
type: 'ls',
template: ['type', 'sessionId', 'path']
template: ['type', 'sessionId', 'showHiddenFile', 'path']
},
};

View File

@@ -295,6 +295,8 @@ export interface ISftpSession extends ITerminalSession {
// 初始化
init: (resolver: ISftpSessionResolver) => void;
// 设置显示隐藏文件
setShowHiddenFile: (show: boolean) => void;
// 查询文件列表
list: (path: string | undefined) => void;
}