✨ 删除文件.
This commit is contained in:
@@ -79,4 +79,6 @@ public interface ErrorMessage {
|
||||
|
||||
String PATH_NOT_NORMALIZE = "路径不合法";
|
||||
|
||||
String OPERATE_ERROR = "操作失败";
|
||||
|
||||
}
|
||||
|
||||
@@ -104,8 +104,8 @@ public enum InputTypeEnum {
|
||||
*/
|
||||
SFTP_REMOVE("rm",
|
||||
SftpRemoveHandler.class,
|
||||
new String[]{"type", "sessionId", "paths"},
|
||||
SftpRemoveRequest.class),
|
||||
new String[]{"type", "sessionId", "path"},
|
||||
SftpBaseRequest.class),
|
||||
|
||||
/**
|
||||
* SFTP 截断文件
|
||||
|
||||
@@ -48,12 +48,12 @@ public enum OutputTypeEnum {
|
||||
/**
|
||||
* SFTP 创建文件夹
|
||||
*/
|
||||
SFTP_MKDIR("md", "${type}|${sessionId}|${result}|${msg}"),
|
||||
SFTP_MKDIR("mk", "${type}|${sessionId}|${result}|${msg}"),
|
||||
|
||||
/**
|
||||
* SFTP 创建文件
|
||||
*/
|
||||
SFTP_TOUCH("to", "${type}|${sessionId}${result}|${msg}"),
|
||||
SFTP_TOUCH("to", "${type}|${sessionId}|${result}|${msg}"),
|
||||
|
||||
/**
|
||||
* SFTP 移动文件
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.orion.ops.module.asset.handler.host.terminal.handler;
|
||||
|
||||
import com.orion.ops.framework.common.enums.BooleanBit;
|
||||
import com.orion.ops.module.asset.handler.host.terminal.enums.OutputTypeEnum;
|
||||
import com.orion.ops.module.asset.handler.host.terminal.model.request.SftpRemoveRequest;
|
||||
import com.orion.ops.module.asset.handler.host.terminal.model.request.SftpBaseRequest;
|
||||
import com.orion.ops.module.asset.handler.host.terminal.model.response.SftpBaseResponse;
|
||||
import com.orion.ops.module.asset.handler.host.terminal.session.ISftpSession;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -18,14 +18,14 @@ import org.springframework.web.socket.WebSocketSession;
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class SftpRemoveHandler extends AbstractTerminalHandler<SftpRemoveRequest> {
|
||||
public class SftpRemoveHandler extends AbstractTerminalHandler<SftpBaseRequest> {
|
||||
|
||||
@Override
|
||||
public void handle(WebSocketSession channel, SftpRemoveRequest payload) {
|
||||
public void handle(WebSocketSession channel, SftpBaseRequest payload) {
|
||||
// 获取会话
|
||||
ISftpSession session = terminalManager.getSession(channel.getId(), payload.getSessionId());
|
||||
String[] paths = payload.getPaths().split("\\|");
|
||||
log.info("SftpRemoveHandler-handle session: {}, paths: {}", payload.getSessionId(), paths);
|
||||
String[] paths = payload.getPath().split("\\|");
|
||||
log.info("SftpRemoveHandler-handle session: {}, path: {}", payload.getSessionId(), paths);
|
||||
Exception ex = null;
|
||||
// 删除
|
||||
try {
|
||||
@@ -40,7 +40,7 @@ public class SftpRemoveHandler extends AbstractTerminalHandler<SftpRemoveRequest
|
||||
SftpBaseResponse.builder()
|
||||
.sessionId(payload.getSessionId())
|
||||
.result(BooleanBit.of(ex == null).getValue())
|
||||
.msg(ex == null ? null : ex.getMessage())
|
||||
.msg(this.getErrorMessage(ex))
|
||||
.build());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.orion.ops.module.asset.handler.host.terminal.model.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* sftp 删除文件 实体对象
|
||||
* <p>
|
||||
* i|eff00a1|paths
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2024/2/6 13:31
|
||||
*/
|
||||
@Data
|
||||
@SuperBuilder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(name = "SftpRemoveRequest", description = "sftp 删除文件 实体对象")
|
||||
public class SftpRemoveRequest extends SftpBaseRequest {
|
||||
|
||||
@Schema(description = "paths 多个用|分割")
|
||||
private String paths;
|
||||
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -68,6 +69,7 @@ public class SftpSession extends TerminalSession implements ISftpSession {
|
||||
true);
|
||||
return files.stream()
|
||||
.map(SftpSession::fileMapping)
|
||||
.sorted(Comparator.comparing(SftpFileVO::getName))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
6
orion-ops-ui/components.d.ts
vendored
6
orion-ops-ui/components.d.ts
vendored
@@ -5,11 +5,11 @@
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import '@vue/runtime-core';
|
||||
|
||||
export {}
|
||||
export {};
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
RouterLink: typeof import('vue-router')['RouterLink'];
|
||||
RouterView: typeof import('vue-router')['RouterView'];
|
||||
}
|
||||
}
|
||||
|
||||
9
orion-ops-ui/src/utils/dom.ts
Normal file
9
orion-ops-ui/src/utils/dom.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
// 设置 ref 自动聚焦
|
||||
export const setAutoFocus = (el: HTMLElement) => {
|
||||
// 自动聚焦
|
||||
nextTick(() => {
|
||||
el && el.focus();
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<a-dropdown class="host-space-context-menu"
|
||||
<a-dropdown class="terminal-context-menu"
|
||||
:popup-max-height="false"
|
||||
trigger="contextMenu"
|
||||
position="bl"
|
||||
@@ -12,35 +12,35 @@
|
||||
<div class="snippet-item-title">
|
||||
<!-- 名称 -->
|
||||
<span class="snippet-item-title-name">
|
||||
{{ item.name }}
|
||||
</span>
|
||||
<!-- 操作 -->
|
||||
<div class="snippet-item-title-actions">
|
||||
<a-space>
|
||||
<!-- 粘贴 -->
|
||||
<a-tag class="pointer usn"
|
||||
size="small"
|
||||
:checkable="true"
|
||||
:checked="true"
|
||||
@click.stop.prevent="paste">
|
||||
<template #icon>
|
||||
<icon-paste />
|
||||
</template>
|
||||
粘贴
|
||||
</a-tag>
|
||||
<!-- 执行 -->
|
||||
<a-tag class="pointer usn"
|
||||
size="small"
|
||||
:checkable="true"
|
||||
:checked="true"
|
||||
@click.stop="exec">
|
||||
<template #icon>
|
||||
<icon-thunderbolt />
|
||||
</template>
|
||||
执行
|
||||
</a-tag>
|
||||
</a-space>
|
||||
</div>
|
||||
{{ item.name }}
|
||||
</span>
|
||||
<!-- 操作 -->
|
||||
<div class="snippet-item-title-actions">
|
||||
<a-space>
|
||||
<!-- 粘贴 -->
|
||||
<a-tag class="pointer usn"
|
||||
size="small"
|
||||
:checkable="true"
|
||||
:checked="true"
|
||||
@click.stop.prevent="paste">
|
||||
<template #icon>
|
||||
<icon-paste />
|
||||
</template>
|
||||
粘贴
|
||||
</a-tag>
|
||||
<!-- 执行 -->
|
||||
<a-tag class="pointer usn"
|
||||
size="small"
|
||||
:checkable="true"
|
||||
:checked="true"
|
||||
@click.stop="exec">
|
||||
<template #icon>
|
||||
<icon-thunderbolt />
|
||||
</template>
|
||||
执行
|
||||
</a-tag>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 命令 -->
|
||||
<span class="snippet-item-command"
|
||||
@@ -53,35 +53,35 @@
|
||||
<template #content>
|
||||
<!-- 复制 -->
|
||||
<a-doption @click="copyCommand">
|
||||
<div class="host-space-context-menu-icon">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<icon-copy />
|
||||
</div>
|
||||
<div>复制</div>
|
||||
</a-doption>
|
||||
<!-- 粘贴 -->
|
||||
<a-doption @click="paste">
|
||||
<div class="host-space-context-menu-icon">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<icon-paste />
|
||||
</div>
|
||||
<div>粘贴</div>
|
||||
</a-doption>
|
||||
<!-- 执行 -->
|
||||
<a-doption @click="exec">
|
||||
<div class="host-space-context-menu-icon">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<icon-thunderbolt />
|
||||
</div>
|
||||
<div>执行</div>
|
||||
</a-doption>
|
||||
<!-- 修改 -->
|
||||
<a-doption @click="openUpdateSnippet(item)">
|
||||
<div class="host-space-context-menu-icon">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<icon-edit />
|
||||
</div>
|
||||
<div>修改</div>
|
||||
</a-doption>
|
||||
<!-- 删除 -->
|
||||
<a-doption @click="removeSnippet(item.id)">
|
||||
<div class="host-space-context-menu-icon">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<icon-delete />
|
||||
</div>
|
||||
<div>删除</div>
|
||||
@@ -89,7 +89,7 @@
|
||||
<!-- 展开 -->
|
||||
<a-doption v-if="!item.expand"
|
||||
@click="() => item.expand = true">
|
||||
<div class="host-space-context-menu-icon">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<icon-expand />
|
||||
</div>
|
||||
<div>展开</div>
|
||||
@@ -97,7 +97,7 @@
|
||||
<!-- 收起 -->
|
||||
<a-doption v-else
|
||||
@click="() => item.expand = false">
|
||||
<div class="host-space-context-menu-icon">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<icon-shrink />
|
||||
</div>
|
||||
<div>收起</div>
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
import { dataColor } from '@/utils';
|
||||
import { tagColor } from '@/views/asset/host-list/types/const';
|
||||
import { updateHostAlias } from '@/api/asset/host-extra';
|
||||
import { openSshModalKey, PanelSessionType } from '../../types/terminal.const';
|
||||
import { openSshSettingModalKey, PanelSessionType } from '../../types/terminal.const';
|
||||
import { useTerminalStore } from '@/store';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -231,7 +231,7 @@
|
||||
};
|
||||
|
||||
// 打开配置
|
||||
const openSetting = inject(openSshModalKey) as (record: HostQueryResponse) => void;
|
||||
const openSetting = inject(openSshSettingModalKey) as (record: HostQueryResponse) => void;
|
||||
|
||||
// 设置收藏
|
||||
const setFavorite = async (item: HostQueryResponse) => {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, provide, ref, watch } from 'vue';
|
||||
import { NewConnectionType, openSshModalKey } from '../../types/terminal.const';
|
||||
import { NewConnectionType, openSshSettingModalKey } from '../../types/terminal.const';
|
||||
import { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
|
||||
import { HostQueryResponse } from '@/api/asset/host';
|
||||
import HostGroupView from './host-group-view.vue';
|
||||
@@ -57,7 +57,7 @@
|
||||
const sshModal = ref();
|
||||
|
||||
// 暴露打开 ssh 配置模态框
|
||||
provide(openSshModalKey, (record: any) => {
|
||||
provide(openSshSettingModalKey, (record: any) => {
|
||||
sshModal.value?.open(record);
|
||||
});
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<!-- 启用-修改中 -->
|
||||
<a-input v-if="item.editable && item.enabled"
|
||||
v-model="item.shortcutKey"
|
||||
:ref="setEditRef as unknown as VNodeRef"
|
||||
:ref="setAutoFocus as unknown as VNodeRef"
|
||||
class="trigger-input"
|
||||
size="small"
|
||||
placeholder="请按下快捷键"
|
||||
@@ -67,7 +67,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { TerminalShortcutKeyEditable } from '@/store/modules/terminal/types';
|
||||
import type { VNodeRef } from 'vue';
|
||||
import { nextTick } from 'vue';
|
||||
import { setAutoFocus } from '@/utils/dom';
|
||||
|
||||
defineProps<{
|
||||
title: string;
|
||||
@@ -77,14 +77,6 @@
|
||||
|
||||
const emits = defineEmits(['setEditable', 'clearEditable', 'updateEnabled']);
|
||||
|
||||
// 设置 ref
|
||||
const setEditRef = (el: HTMLElement) => {
|
||||
// 自动聚焦
|
||||
nextTick(() => {
|
||||
el && el.focus();
|
||||
});
|
||||
};
|
||||
|
||||
// 修改启用状态
|
||||
const updateEnabledStatus = (item: TerminalShortcutKeyEditable, enabled: boolean) => {
|
||||
emits('updateEnabled', item, enabled);
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
<template>
|
||||
<div></div>
|
||||
<a-modal v-model:visible="visible"
|
||||
body-class="modal-form"
|
||||
title-align="start"
|
||||
:title="touch ? '创建文件' : '创建文件夹'"
|
||||
:align-center="false"
|
||||
:mask-closable="false"
|
||||
:unmount-on-close="true"
|
||||
:on-before-ok="handlerOk"
|
||||
@close="handleClose">
|
||||
<a-form :model="formModel"
|
||||
ref="formRef"
|
||||
label-align="right"
|
||||
layout="horizontal"
|
||||
:style="{ width: '460px' }"
|
||||
:label-col-props="{ span: 6 }"
|
||||
:wrapper-col-props="{ span: 18 }">
|
||||
<!-- 文件路径 -->
|
||||
<a-form-item field="path"
|
||||
label="文件路径"
|
||||
:rules="[{ required: true, message: '请输入文件路径' }]">
|
||||
<a-input ref="pathRef"
|
||||
v-model="formModel.path"
|
||||
placeholder="请输入文件路径" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -9,6 +34,76 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useVisible from '@/hooks/visible';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { nextTick, ref } from 'vue';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import SftpSession from '../../handler/sftp-session';
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { sessionManager } = useTerminalStore();
|
||||
|
||||
const sessionId = ref();
|
||||
const touch = ref(false);
|
||||
const pathRef = ref();
|
||||
const formRef = ref();
|
||||
const formModel = ref({
|
||||
path: ''
|
||||
});
|
||||
|
||||
// 打开新增
|
||||
const open = (session: string, path: string, isTouch: boolean) => {
|
||||
sessionId.value = session;
|
||||
formModel.value.path = path;
|
||||
touch.value = isTouch;
|
||||
setVisible(true);
|
||||
// 自动聚焦
|
||||
nextTick(() => {
|
||||
pathRef.value?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
|
||||
// 确定
|
||||
const handlerOk = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 验证参数
|
||||
const error = await formRef.value.validate();
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
// 获取会话
|
||||
const session = sessionManager.getSession(sessionId.value);
|
||||
if (session instanceof SftpSession) {
|
||||
if (touch.value) {
|
||||
// 创建文件
|
||||
session.touch(formModel.value.path);
|
||||
} else {
|
||||
// 创建文件夹
|
||||
session.mkdir(formModel.value.path);
|
||||
}
|
||||
}
|
||||
// 清空
|
||||
handlerClear();
|
||||
} catch (e) {
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
handlerClear();
|
||||
};
|
||||
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<!-- 根目录 -->
|
||||
<a-breadcrumb-item class="sftp-path-unit"
|
||||
@click.stop="loadFileList('/')">
|
||||
<icon-home />
|
||||
<icon-storage />
|
||||
</a-breadcrumb-item>
|
||||
<!-- 子目录 -->
|
||||
<a-breadcrumb-item class="sftp-path-unit"
|
||||
@@ -180,8 +180,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PathAnalysis } from '@/utils/file';
|
||||
import type { ISftpSession } from '../../types/terminal.type';
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
import { inject, nextTick, ref, watch } from 'vue';
|
||||
import { getParentPath, getPathAnalysis } from '@/utils/file';
|
||||
import { openSftpCreateModalKey } from '../../types/terminal.const';
|
||||
|
||||
const props = defineProps<{
|
||||
currentPath: string;
|
||||
@@ -197,6 +198,8 @@
|
||||
const pathInput = ref('');
|
||||
const pathInputRef = ref();
|
||||
|
||||
const openSftpCreateModal = inject(openSftpCreateModalKey) as (sessionId: string, path: string, isTouch: boolean) => void;
|
||||
|
||||
// 监听路径变化
|
||||
watch(() => props.currentPath, (path) => {
|
||||
if (path) {
|
||||
@@ -246,20 +249,19 @@
|
||||
|
||||
// 创建文件
|
||||
const createFile = () => {
|
||||
// TODO openModal(true, "props.currentPath")
|
||||
console.log(props.currentPath);
|
||||
openSftpCreateModal(props.session?.sessionId as string, props.currentPath + '/', true);
|
||||
};
|
||||
|
||||
// 创建文件夹
|
||||
const createDir = () => {
|
||||
// TODO openModal(false, "props.currentPath")
|
||||
console.log(props.currentPath);
|
||||
openSftpCreateModal(props.session?.sessionId as string, props.currentPath + '/', false);
|
||||
};
|
||||
|
||||
// 删除选中文件
|
||||
const deleteSelectFiles = () => {
|
||||
// TODO confirm
|
||||
console.log(props.selectedFiles);
|
||||
if (props.selectedFiles?.length) {
|
||||
props.session?.remove(props.selectedFiles);
|
||||
}
|
||||
};
|
||||
|
||||
// 上传文件
|
||||
@@ -279,8 +281,8 @@
|
||||
<style lang="less" scoped>
|
||||
@action-num: 7;
|
||||
@action-gap: 8px;
|
||||
@action-width: 26px;
|
||||
@actions-width: @action-num * (@action-width + @action-gap);
|
||||
@action-size: 26px;
|
||||
@actions-width: @action-num * (@action-size + @action-gap);
|
||||
|
||||
.sftp-table-header {
|
||||
width: 100%;
|
||||
@@ -312,6 +314,7 @@
|
||||
|
||||
:deep(.sftp-path-unit) {
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
color: rgb(var(--arcoblue-6));
|
||||
@@ -326,5 +329,7 @@
|
||||
.header-action-icon {
|
||||
font-size: 16px;
|
||||
padding: 4px;
|
||||
width: @action-size;
|
||||
height: @action-size;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<a-table row-key="path"
|
||||
ref="tableRef"
|
||||
class="sftp-table"
|
||||
label-align="left"
|
||||
:columns="columns"
|
||||
@@ -9,16 +10,19 @@
|
||||
:data="list"
|
||||
:pagination="false"
|
||||
:bordered="false"
|
||||
@cell-mouse-enter="setEditable"
|
||||
@cell-mouse-leave="unsetEditable">
|
||||
:loading="loading"
|
||||
@cell-mouse-enter="setOperable"
|
||||
@cell-mouse-leave="unsetOperable">
|
||||
<!-- 文件搜索框 -->
|
||||
<template #nameFilter="{ filterValue, setFilterValue, handleFilterConfirm, handleFilterReset}">
|
||||
<div class="name-filter">
|
||||
<a-space direction="vertical">
|
||||
<!-- 过滤输入框 -->
|
||||
<a-input size="small"
|
||||
:ref="setAutoFocus as unknown as VNodeRef"
|
||||
:model-value="filterValue[0]"
|
||||
@input="(value) => setFilterValue([value])" />
|
||||
@input="(value) => setFilterValue([value])"
|
||||
@pressEnter="handleFilterConfirm" />
|
||||
<!-- 按钮 -->
|
||||
<div class="name-filter-footer">
|
||||
<a-button size="small" @click="handleFilterConfirm">过滤</a-button>
|
||||
@@ -59,10 +63,24 @@
|
||||
arrow-class="terminal-tooltip-content"
|
||||
content="复制路径">
|
||||
<span class="click-icon-wrapper row-action-icon"
|
||||
@click="copy(record.path, false)">
|
||||
@click="copy(record.path, '已复制')">
|
||||
<icon-copy />
|
||||
</span>
|
||||
</a-tooltip>
|
||||
<!-- 编辑内容 -->
|
||||
<a-tooltip v-if="canEditable(record.attr)"
|
||||
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 row-action-icon"
|
||||
@click="editFile(record)">
|
||||
<icon-edit />
|
||||
</span>
|
||||
</a-tooltip>
|
||||
<!-- 删除 -->
|
||||
<a-tooltip position="top"
|
||||
:mini="true"
|
||||
@@ -125,12 +143,14 @@
|
||||
<script lang="ts" setup>
|
||||
import type { TableData } from '@arco-design/web-vue/es/table/interface';
|
||||
import type { SftpFile, ISftpSession } from '../../types/terminal.type';
|
||||
import { ref, computed } from 'vue';
|
||||
import type { VNodeRef } from 'vue';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { useRowSelection } from '@/types/table';
|
||||
import { dateFormat } from '@/utils';
|
||||
import columns from './types/table.columns';
|
||||
import useCopy from '@/hooks/copy';
|
||||
import columns from './types/table.columns';
|
||||
import { FILE_TYPE } from '../../types/terminal.const';
|
||||
import { setAutoFocus } from '@/utils/dom';
|
||||
|
||||
const props = defineProps<{
|
||||
session: ISftpSession | undefined;
|
||||
@@ -144,6 +164,11 @@
|
||||
const rowSelection = useRowSelection({ width: 40 });
|
||||
const { copy } = useCopy();
|
||||
|
||||
// 切换页面自动清空过滤
|
||||
watch(() => props.list, () => {
|
||||
tableRef.value?.clearFilters();
|
||||
});
|
||||
|
||||
const selectedKeys = computed({
|
||||
get() {
|
||||
return props.selectedFiles;
|
||||
@@ -154,15 +179,16 @@
|
||||
});
|
||||
|
||||
const editRowName = ref<string>('');
|
||||
const tableRef = ref();
|
||||
|
||||
// 设置选中状态
|
||||
const setEditable = (record: TableData) => {
|
||||
// 设置可操作状态
|
||||
const setOperable = (record: TableData) => {
|
||||
editRowName.value = record.name;
|
||||
record.hover = true;
|
||||
};
|
||||
|
||||
// 设置未选中状态
|
||||
const unsetEditable = (record: TableData) => {
|
||||
// 设置不可操作状态
|
||||
const unsetOperable = (record: TableData) => {
|
||||
setTimeout(() => {
|
||||
// 等待后如果还是当前行 但是未被选中则代表已经被失焦
|
||||
if (record.name === editRowName.value && !record.hover) {
|
||||
@@ -172,21 +198,30 @@
|
||||
record.hover = false;
|
||||
};
|
||||
|
||||
// 是否可编辑
|
||||
const canEditable = (attr: string) => {
|
||||
const typeValue = formatFileType(attr).value;
|
||||
// 非文件夹和链接文件可以编辑
|
||||
return FILE_TYPE.DIRECTORY.value !== typeValue
|
||||
&& FILE_TYPE.LINK_FILE.value !== typeValue;
|
||||
};
|
||||
|
||||
// 点击文件名称
|
||||
const clickFilename = (record: TableData) => {
|
||||
if (FILE_TYPE.DIRECTORY.value === formatFileType(record.attr).value) {
|
||||
// 进入文件夹
|
||||
emits('loadFile', record.path);
|
||||
} else {
|
||||
// 点击文件
|
||||
// TODO VIEW
|
||||
}
|
||||
};
|
||||
|
||||
// 编辑文件
|
||||
const editFile = (record: TableData) => {
|
||||
// TODO
|
||||
};
|
||||
|
||||
// 删除文件
|
||||
const deleteFile = (path: string) => {
|
||||
// TODO confirm
|
||||
console.log(path);
|
||||
props.session?.remove([path]);
|
||||
};
|
||||
|
||||
// 下载文件
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
<!-- 左侧面板表格 -->
|
||||
<template #first>
|
||||
<a-spin class="sftp-table-container"
|
||||
:loading="tableLoading">
|
||||
:loading="tableLoading"
|
||||
:hide-icon="true">
|
||||
<!-- 表头 -->
|
||||
<sftp-table-header class="sftp-table-header"
|
||||
:current-path="currentPath"
|
||||
@@ -27,6 +28,8 @@
|
||||
<div>editor</div>
|
||||
</template>
|
||||
</a-split>
|
||||
<!-- 创建文件模态框 -->
|
||||
<sftp-create-modal ref="createModal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -38,12 +41,14 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ISftpSession, SftpFile, TerminalTabItem } from '../../types/terminal.type';
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import { onMounted, onUnmounted, provide, ref } from 'vue';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { openSftpCreateModalKey } from '../../types/terminal.const';
|
||||
import SftpTable from './sftp-table.vue';
|
||||
import SftpTableHeader from './sftp-table-header.vue';
|
||||
import SftpCreateModal from './sftp-create-modal.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
tab: TerminalTabItem
|
||||
@@ -53,12 +58,18 @@
|
||||
const { loading: tableLoading, setLoading: setTableLoading } = useLoading(true);
|
||||
|
||||
const session = ref<ISftpSession>();
|
||||
const createModal = ref();
|
||||
const currentPath = ref<string>('');
|
||||
const fileList = ref<Array<SftpFile>>([]);
|
||||
const selectFiles = ref<Array<string>>([]);
|
||||
const splitSize = ref(1);
|
||||
const editView = ref(true);
|
||||
|
||||
// 暴露打开创建方法
|
||||
provide(openSftpCreateModalKey, (sessionId: string, path: string, isTouch: boolean) => {
|
||||
createModal.value?.open(sessionId, path, isTouch);
|
||||
});
|
||||
|
||||
// 连接成功回调
|
||||
const connectCallback = () => {
|
||||
loadFiles(undefined);
|
||||
@@ -70,16 +81,44 @@
|
||||
session.value?.list(path);
|
||||
};
|
||||
|
||||
// 检查结果
|
||||
const checkResult = (result: string, msg: string): boolean => {
|
||||
const success = !!Number.parseInt(result);
|
||||
if (!success) {
|
||||
Message.error(msg);
|
||||
}
|
||||
return success;
|
||||
};
|
||||
|
||||
// 接收列表回调
|
||||
const resolveList = (result: string, path: string, list: Array<SftpFile>) => {
|
||||
const success = !!Number.parseInt(result);
|
||||
setTableLoading(false);
|
||||
if (!success) {
|
||||
Message.error('查询失败');
|
||||
if (!checkResult(result, '查询失败')) {
|
||||
return;
|
||||
}
|
||||
currentPath.value = path;
|
||||
fileList.value = list;
|
||||
selectFiles.value = [];
|
||||
};
|
||||
|
||||
// 接收文件响应
|
||||
const resolveFileAction = (result: string, msg: string) => {
|
||||
setTableLoading(false);
|
||||
// 检查结果
|
||||
if (!checkResult(result, msg)) {
|
||||
return;
|
||||
}
|
||||
Message.success('操作成功');
|
||||
// 刷新列表
|
||||
loadFiles(currentPath.value);
|
||||
};
|
||||
|
||||
// 接收获取文件内容响应
|
||||
const resolveSftpGetContent = (path: string, result: string, content: string) => {
|
||||
};
|
||||
|
||||
// 接收修改文件内容响应
|
||||
const resolveSftpSetContent = (result: string, msg: string) => {
|
||||
};
|
||||
|
||||
// 初始化会话
|
||||
@@ -87,7 +126,14 @@
|
||||
// 创建终端处理器
|
||||
session.value = await sessionManager.openSftp(props.tab, {
|
||||
connectCallback,
|
||||
resolveList
|
||||
resolveList,
|
||||
resolveSftpMkdir: resolveFileAction,
|
||||
resolveSftpTouch: resolveFileAction,
|
||||
resolveSftpMove: resolveFileAction,
|
||||
resolveSftpRemove: resolveFileAction,
|
||||
resolveSftpChmod: resolveFileAction,
|
||||
resolveSftpGetContent,
|
||||
resolveSftpSetContent,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ const columns = [
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
width: 218,
|
||||
width: 234,
|
||||
cellClass: 'action-cell',
|
||||
},
|
||||
] as TableColumnData[];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ISftpSession, ISftpSessionResolver, ITerminalChannel } from '../types/terminal.type';
|
||||
import { InputProtocol } from '../types/terminal.protocol';
|
||||
import { Modal } from '@arco-design/web-vue';
|
||||
|
||||
// sftp 会话实现
|
||||
export default class SftpSession implements ISftpSession {
|
||||
@@ -53,6 +54,71 @@ export default class SftpSession implements ISftpSession {
|
||||
});
|
||||
};
|
||||
|
||||
// 创建文件夹
|
||||
mkdir(path: string) {
|
||||
this.channel.send(InputProtocol.SFTP_MKDIR, {
|
||||
sessionId: this.sessionId,
|
||||
path
|
||||
});
|
||||
};
|
||||
|
||||
// 创建文件
|
||||
touch(path: string) {
|
||||
this.channel.send(InputProtocol.SFTP_TOUCH, {
|
||||
sessionId: this.sessionId,
|
||||
path
|
||||
});
|
||||
};
|
||||
|
||||
// 移动文件
|
||||
move(path: string, target: string) {
|
||||
this.channel.send(InputProtocol.SFTP_MOVE, {
|
||||
sessionId: this.sessionId,
|
||||
path,
|
||||
target
|
||||
});
|
||||
};
|
||||
|
||||
// 删除文件
|
||||
remove(path: string[]) {
|
||||
Modal.confirm({
|
||||
title: '删除确认',
|
||||
content: `确定要删除 ${path} 吗? 确定后将立即删除且无法恢复!`,
|
||||
onOk: () => {
|
||||
this.channel.send(InputProtocol.SFTP_REMOVE, {
|
||||
sessionId: this.sessionId,
|
||||
path: path.join('|')
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 修改权限
|
||||
chmod(path: string, mod: number) {
|
||||
this.channel.send(InputProtocol.SFTP_CHMOD, {
|
||||
sessionId: this.sessionId,
|
||||
path,
|
||||
mod
|
||||
});
|
||||
};
|
||||
|
||||
// 获取内容
|
||||
getContent(path: string) {
|
||||
this.channel.send(InputProtocol.SFTP_GET_CONTENT, {
|
||||
sessionId: this.sessionId,
|
||||
path
|
||||
});
|
||||
};
|
||||
|
||||
// 修改内容
|
||||
setContent(path: string, content: string) {
|
||||
this.channel.send(InputProtocol.SFTP_SET_CONTENT, {
|
||||
sessionId: this.sessionId,
|
||||
path,
|
||||
content
|
||||
});
|
||||
};
|
||||
|
||||
// 断开连接
|
||||
disconnect(): void {
|
||||
// 发送关闭消息
|
||||
|
||||
@@ -119,4 +119,53 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
|
||||
session && session.resolver.resolveList(result, path, JSON.parse(body));
|
||||
}
|
||||
|
||||
// 处理 SFTP 创建文件夹
|
||||
processSftpMkdir({ sessionId, result, msg }: OutputPayload): void {
|
||||
// 获取会话
|
||||
const session = this.sessionManager.getSession<ISftpSession>(sessionId);
|
||||
session && session.resolver.resolveSftpMkdir(result, msg);
|
||||
}
|
||||
|
||||
// 处理 SFTP 创建文件
|
||||
processSftpTouch({ sessionId, result, msg }: OutputPayload): void {
|
||||
// 获取会话
|
||||
const session = this.sessionManager.getSession<ISftpSession>(sessionId);
|
||||
session && session.resolver.resolveSftpTouch(result, msg);
|
||||
}
|
||||
|
||||
// 处理 SFTP 移动文件
|
||||
processSftpMove({ sessionId, result, msg }: OutputPayload): void {
|
||||
// 获取会话
|
||||
const session = this.sessionManager.getSession<ISftpSession>(sessionId);
|
||||
session && session.resolver.resolveSftpMove(result, msg);
|
||||
}
|
||||
|
||||
// 处理 SFTP 删除文件
|
||||
processSftpRemove({ sessionId, result, msg }: OutputPayload): void {
|
||||
// 获取会话
|
||||
const session = this.sessionManager.getSession<ISftpSession>(sessionId);
|
||||
session && session.resolver.resolveSftpRemove(result, msg);
|
||||
}
|
||||
|
||||
// 处理 SFTP 修改文件权限
|
||||
processSftpChmod({ sessionId, result, msg }: OutputPayload): void {
|
||||
// 获取会话
|
||||
const session = this.sessionManager.getSession<ISftpSession>(sessionId);
|
||||
session && session.resolver.resolveSftpChmod(result, msg);
|
||||
}
|
||||
|
||||
// 处理 SFTP 获取文件内容
|
||||
processSftpGetContent({ sessionId, path, result, content }: OutputPayload): void {
|
||||
// 获取会话
|
||||
const session = this.sessionManager.getSession<ISftpSession>(sessionId);
|
||||
session && session.resolver.resolveSftpGetContent(path, result, content);
|
||||
}
|
||||
|
||||
// 处理 SFTP 修改文件内容
|
||||
processSftpSetContent({ sessionId, result, msg }: OutputPayload) {
|
||||
// 获取会话
|
||||
const session = this.sessionManager.getSession<ISftpSession>(sessionId);
|
||||
session && session.resolver.resolveSftpSetContent(result, msg);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,6 +35,41 @@ export const InputProtocol = {
|
||||
type: 'ls',
|
||||
template: ['type', 'sessionId', 'showHiddenFile', 'path']
|
||||
},
|
||||
// SFTP 创建文件夹
|
||||
SFTP_MKDIR: {
|
||||
type: 'mk',
|
||||
template: ['type', 'sessionId', 'path']
|
||||
},
|
||||
// SFTP 创建文件
|
||||
SFTP_TOUCH: {
|
||||
type: 'to',
|
||||
template: ['type', 'sessionId', 'path']
|
||||
},
|
||||
// SFTP 移动文件
|
||||
SFTP_MOVE: {
|
||||
type: 'mv',
|
||||
template: ['type', 'sessionId', 'path', 'target']
|
||||
},
|
||||
// SFTP 删除文件
|
||||
SFTP_REMOVE: {
|
||||
type: 'rm',
|
||||
template: ['type', 'sessionId', 'path']
|
||||
},
|
||||
// SFTP 修改文件权限
|
||||
SFTP_CHMOD: {
|
||||
type: 'cm',
|
||||
template: ['type', 'sessionId', 'path', 'mod']
|
||||
},
|
||||
// SFTP 获取内容
|
||||
SFTP_GET_CONTENT: {
|
||||
type: 'gc',
|
||||
template: ['type', 'sessionId', 'path']
|
||||
},
|
||||
// SFTP 修改内容
|
||||
SFTP_SET_CONTENT: {
|
||||
type: 'sc',
|
||||
template: ['type', 'sessionId', 'path', 'content']
|
||||
},
|
||||
};
|
||||
|
||||
// 输出协议
|
||||
@@ -72,7 +107,49 @@ export const OutputProtocol = {
|
||||
// SFTP 文件列表
|
||||
SFTP_LIST: {
|
||||
type: 'ls',
|
||||
template: ['type', 'sessionId', 'result', 'path', 'body'],
|
||||
template: ['type', 'sessionId', 'path', 'result', 'body'],
|
||||
processMethod: 'processSftpList'
|
||||
},
|
||||
// SFTP 创建文件夹
|
||||
SFTP_MKDIR: {
|
||||
type: 'mk',
|
||||
template: ['type', 'sessionId', 'result', 'msg'],
|
||||
processMethod: 'processSftpMkdir'
|
||||
},
|
||||
// SFTP 创建文件
|
||||
SFTP_TOUCH: {
|
||||
type: 'to',
|
||||
template: ['type', 'sessionId', 'result', 'msg'],
|
||||
processMethod: 'processSftpTouch'
|
||||
},
|
||||
// SFTP 移动文件
|
||||
SFTP_MOVE: {
|
||||
type: 'mv',
|
||||
template: ['type', 'sessionId', 'result', 'msg'],
|
||||
processMethod: 'processSftpMove'
|
||||
},
|
||||
// SFTP 删除文件
|
||||
SFTP_REMOVE: {
|
||||
type: 'rm',
|
||||
template: ['type', 'sessionId', 'result', 'msg'],
|
||||
processMethod: 'processSftpRemove'
|
||||
},
|
||||
// SFTP 修改文件权限
|
||||
SFTP_CHMOD: {
|
||||
type: 'cm',
|
||||
template: ['type', 'sessionId', 'result', 'msg'],
|
||||
processMethod: 'processSftpChmod'
|
||||
},
|
||||
// SFTP 获取文件内容
|
||||
SFTP_GET_CONTENT: {
|
||||
type: 'gc',
|
||||
template: ['type', 'sessionId', 'path', 'result', 'content'],
|
||||
processMethod: 'processSftpGetContent'
|
||||
},
|
||||
// SFTP 修改文件内容
|
||||
SFTP_SET_CONTENT: {
|
||||
type: 'sc',
|
||||
template: ['type', 'sessionId', 'result', 'msg'],
|
||||
processMethod: 'processSftpSetContent'
|
||||
},
|
||||
};
|
||||
|
||||
@@ -180,6 +180,20 @@ export interface ITerminalOutputProcessor {
|
||||
processSshOutput: (payload: OutputPayload) => void;
|
||||
// 处理 SFTP 文件列表
|
||||
processSftpList: (payload: OutputPayload) => void;
|
||||
// 处理 SFTP 创建文件夹
|
||||
processSftpMkdir: (payload: OutputPayload) => void;
|
||||
// 处理 SFTP 创建文件
|
||||
processSftpTouch: (payload: OutputPayload) => void;
|
||||
// 处理 SFTP 移动文件
|
||||
processSftpMove: (payload: OutputPayload) => void;
|
||||
// 处理 SFTP 删除文件
|
||||
processSftpRemove: (payload: OutputPayload) => void;
|
||||
// 处理 SFTP 修改文件权限
|
||||
processSftpChmod: (payload: OutputPayload) => void;
|
||||
// 处理 SFTP 获取文件内容
|
||||
processSftpGetContent: (payload: OutputPayload) => void;
|
||||
// 处理 SFTP 修改文件内容
|
||||
processSftpSetContent: (payload: OutputPayload) => void;
|
||||
}
|
||||
|
||||
// xterm dom 元素引用
|
||||
@@ -299,6 +313,20 @@ export interface ISftpSession extends ITerminalSession {
|
||||
setShowHiddenFile: (show: boolean) => void;
|
||||
// 查询文件列表
|
||||
list: (path: string | undefined) => void;
|
||||
// 创建文件夹
|
||||
mkdir: (path: string) => void;
|
||||
// 创建文件
|
||||
touch: (path: string) => void;
|
||||
// 移动文件
|
||||
move: (path: string, target: string) => void;
|
||||
// 删除文件
|
||||
remove: (path: string[]) => void;
|
||||
// 修改权限
|
||||
chmod: (path: string, mod: number) => void;
|
||||
// 获取内容
|
||||
getContent: (path: string) => void;
|
||||
// 修改内容
|
||||
setContent: (path: string, content: string) => void;
|
||||
}
|
||||
|
||||
// sftp 会话接收器定义
|
||||
@@ -307,6 +335,20 @@ export interface ISftpSessionResolver {
|
||||
connectCallback: () => void;
|
||||
// 接受文件列表响应
|
||||
resolveList: (result: string, path: string, list: Array<SftpFile>) => void;
|
||||
// 接收创建文件夹响应
|
||||
resolveSftpMkdir: (result: string, msg: string) => void;
|
||||
// 接收创建文件响应
|
||||
resolveSftpTouch: (result: string, msg: string) => void;
|
||||
// 接收移动文件响应
|
||||
resolveSftpMove: (result: string, msg: string) => void;
|
||||
// 接收删除文件响应
|
||||
resolveSftpRemove: (result: string, msg: string) => void;
|
||||
// 接收修改文件权限响应
|
||||
resolveSftpChmod: (result: string, msg: string) => void;
|
||||
// 接收获取文件内容响应
|
||||
resolveSftpGetContent: (path: string, result: string, content: string) => void;
|
||||
// 接收修改文件内容响应
|
||||
resolveSftpSetContent: (result: string, msg: string) => void;
|
||||
}
|
||||
|
||||
// sftp 文件
|
||||
|
||||
Reference in New Issue
Block a user