diff --git a/ui/components.d.ts b/ui/components.d.ts
index 690a594e..0f60f39e 100644
--- a/ui/components.d.ts
+++ b/ui/components.d.ts
@@ -42,6 +42,7 @@ declare module 'vue' {
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
NPopover: typeof import('naive-ui')['NPopover']
+ NPopselect: typeof import('naive-ui')['NPopselect']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSpin: typeof import('naive-ui')['NSpin']
diff --git a/ui/src/components/Drawer/components/FileManagement/index.vue b/ui/src/components/Drawer/components/FileManagement/index.vue
index 42373a04..43335d09 100644
--- a/ui/src/components/Drawer/components/FileManagement/index.vue
+++ b/ui/src/components/Drawer/components/FileManagement/index.vue
@@ -1,14 +1,14 @@
@@ -189,13 +187,6 @@ const columns = createColumns();
-
+
-
-
diff --git a/ui/src/components/Drawer/components/FileManagement/widget/index.vue b/ui/src/components/Drawer/components/FileManagement/widget/index.vue
index f4e4f53b..4a6bde7f 100644
--- a/ui/src/components/Drawer/components/FileManagement/widget/index.vue
+++ b/ui/src/components/Drawer/components/FileManagement/widget/index.vue
@@ -3,8 +3,7 @@ import type { DataTableColumns, DropdownOption, UploadCustomRequestOptions, Uplo
import { useI18n } from 'vue-i18n';
import { NText, useMessage } from 'naive-ui';
-import { useWindowSize } from '@vueuse/core';
-import { computed, h, nextTick, onActivated, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue';
+import { computed, h, nextTick, onActivated, onBeforeUnmount, onMounted, provide, ref, watch, watchEffect } from 'vue';
import {
ChevronLeft,
ChevronRight,
@@ -36,11 +35,13 @@ export interface IFilePath {
showArrow: boolean;
}
-withDefaults(
+const props = withDefaults(
defineProps<{
+ fileList: FileManageSftpFileItem[];
columns?: DataTableColumns;
}>(),
{
+ fileList: () => [],
columns: () => [],
}
);
@@ -48,7 +49,6 @@ withDefaults(
const { t } = useI18n();
const message = useMessage();
const fileManageStore = useFileManageStore();
-const { height: _windowHeight } = useWindowSize();
const options: DropdownOption[] = [
{
@@ -81,7 +81,6 @@ const newFileName = ref('');
const searchValue = ref('');
const modalContent = ref('');
const loading = ref(true);
-const showInner = ref(false);
const showModal = ref(false);
const showDropdown = ref(false);
const isShowUploadList = ref(false);
@@ -89,7 +88,6 @@ const disabledBack = ref(true);
const disabledForward = ref(true);
const scrollRef = ref(null);
-const dataList = ref([]);
const filePathList = ref([]);
const currentRowData = ref>({});
@@ -97,108 +95,107 @@ const persistedUploadFiles = ref([]);
const uploadFileList = ref([]);
const stopUploadFile = ref();
-const _tableHeight = computed(() => {
- if (!uploadFileList.value || uploadFileList.value.length === 0) {
- return 240;
+watchEffect(() => {
+ if (props.fileList.length > 0) {
+ loading.value = false;
}
- return 300;
});
-watch(
- () => fileManageStore.currentPath,
- newPath => {
- if (newPath) {
- // 重置现有路径列表
- filePathList.value = [];
-
- if (newPath === '/') {
- disabledBack.value = true;
- return;
- }
-
- if (fileManageStore.currentPath === forwardPath.value) {
- disabledForward.value = true;
- }
-
- // 分割路径
- const pathSegments = newPath.split('/').filter(segment => segment);
-
- // 根据路径段构建完整的路径列表
- let currentPath = '';
- pathSegments.forEach((segment, index) => {
- // 更新当前路径
- currentPath += `/${segment}`;
-
- // 添加到路径列表
- filePathList.value.push({
- id: currentPath, // 使用完整路径作为ID
- path: segment, // 显示路径段名称
- active: index === pathSegments.length - 1,
- showArrow: index !== pathSegments.length - 1,
- });
- });
-
- // 滚动到最后一个路径段
- nextTick(() => {
- const contentRef = document.getElementsByClassName('n-scrollbar-content')[2];
- if (scrollRef.value && contentRef) {
- // @ts-expect-error 目标对象滚动
- scrollRef.value.scrollTo({
- left: contentRef.scrollWidth,
- behavior: 'smooth',
- });
- }
- });
- }
- },
- {
- immediate: true,
- }
-);
-
-watch(
- () => forwardPath.value,
- (newPath, oldPath) => {
- if (oldPath && (oldPath === newPath || oldPath.startsWith(`${newPath}/`))) {
- // 如果 oldPath 包含 newPath,则重置 forwardPath 为 oldPath
- forwardPath.value = oldPath;
- }
- }
-);
-
-watch(
- () => fileManageStore.fileList,
- newFileList => {
- if (newFileList) {
- loading.value = false;
- dataList.value = newFileList;
- }
- },
- {
- immediate: true,
- }
-);
-
-watch(
- () => uploadFileList.value,
- newValue => {
- if (newValue && newValue.length > 0) {
- persistedUploadFiles.value = [...newValue];
- }
- },
- { deep: true }
-);
-
-watch(
- () => searchValue.value,
- (newVal: string) => {
- if (newVal) {
- dataList.value = fileManageStore.fileList!.filter(item => item.name.toLowerCase().includes(newVal.toLowerCase()));
- } else {
- dataList.value = fileManageStore.fileList!;
- }
- }
-);
+// watch(
+// () => fileManageStore.currentPath,
+// newPath => {
+// if (newPath) {
+// // 重置现有路径列表
+// filePathList.value = [];
+
+// if (newPath === '/') {
+// disabledBack.value = true;
+// return;
+// }
+
+// if (fileManageStore.currentPath === forwardPath.value) {
+// disabledForward.value = true;
+// }
+
+// // 分割路径
+// const pathSegments = newPath.split('/').filter(segment => segment);
+
+// // 根据路径段构建完整的路径列表
+// let currentPath = '';
+// pathSegments.forEach((segment, index) => {
+// // 更新当前路径
+// currentPath += `/${segment}`;
+
+// // 添加到路径列表
+// filePathList.value.push({
+// id: currentPath, // 使用完整路径作为ID
+// path: segment, // 显示路径段名称
+// active: index === pathSegments.length - 1,
+// showArrow: index !== pathSegments.length - 1,
+// });
+// });
+
+// // 滚动到最后一个路径段
+// nextTick(() => {
+// const contentRef = document.getElementsByClassName('n-scrollbar-content')[2];
+// if (scrollRef.value && contentRef) {
+// // @ts-expect-error 目标对象滚动
+// scrollRef.value.scrollTo({
+// left: contentRef.scrollWidth,
+// behavior: 'smooth',
+// });
+// }
+// });
+// }
+// },
+// {
+// immediate: true,
+// }
+// );
+
+// watch(
+// () => forwardPath.value,
+// (newPath, oldPath) => {
+// if (oldPath && (oldPath === newPath || oldPath.startsWith(`${newPath}/`))) {
+// // 如果 oldPath 包含 newPath,则重置 forwardPath 为 oldPath
+// forwardPath.value = oldPath;
+// }
+// }
+// );
+
+// watch(
+// () => fileManageStore.fileList,
+// newFileList => {
+// if (newFileList) {
+// loading.value = false;
+// dataList.value = newFileList;
+// }
+// },
+// {
+// immediate: true,
+// }
+// );
+
+// watch(
+// () => uploadFileList.value,
+// newValue => {
+// if (newValue && newValue.length > 0) {
+// persistedUploadFiles.value = [...newValue];
+// }
+// },
+// { deep: true }
+// );
+
+// watch(
+// () => searchValue.value,
+// (newVal: string) => {
+// if (newVal) {
+// dataList.value = fileManageStore.fileList!.filter(item => item.name.toLowerCase().includes(newVal.toLowerCase()));
+// } else {
+// dataList.value = fileManageStore.fileList!;
+// }
+// }
+// );
const onClickOutside = () => {
showDropdown.value = false;
@@ -216,7 +213,7 @@ const handleRemoveItem = (data: { file: UploadFileInfo; fileList: UploadFileInfo
// 对于上传失败、已完成或其他状态的文件,允许直接移除
uploadFileList.value = fileList.filter(item => item.id !== file.id);
- fileManageStore.setUploadFileList(uploadFileList.value);
+ // fileManageStore.setUploadFileList(uploadFileList.value);
return true;
};
@@ -244,11 +241,11 @@ const handleSelect = (key: string) => {
break;
}
case 'download': {
- mittBus.emit('download-file', {
- path: `${fileManageStore.currentPath}/${currentRowData?.value?.name as string}`,
- is_dir: currentRowData.value.is_dir!,
- size: currentRowData.value.size!,
- });
+ // mittBus.emit('download-file', {
+ // path: `${fileManageStore.currentPath}/${currentRowData?.value?.name as string}`,
+ // is_dir: currentRowData.value.is_dir!,
+ // size: currentRowData.value.size!,
+ // });
break;
}
@@ -439,11 +436,6 @@ const handleUploadFileChange = (options: { fileList: Array }) =>
if (options.fileList.length > 0) {
uploadFileList.value = options.fileList;
fileManageStore.setUploadFileList(options.fileList);
-
- // 使用 nextTick 确保数据更新后再打开抽屉
- nextTick(() => {
- showInner.value = true;
- });
}
};
@@ -474,18 +466,6 @@ const customRequest = ({ file, onFinish, onError, onProgress }: UploadCustomRequ
});
};
-/**
- * @description 打开传输历史列表
- */
-// const handleOpenTransferList = () => {
-// 从 store 中恢复文件列表
-// uploadFileList.value = [...fileManageStore.uploadFileList];
-
-// nextTick(() => {
-// showInner.value = true;
-// });
-// };
-
const modalNegativeClick = () => {
newFileName.value = '';
};
@@ -673,50 +653,8 @@ provide('persistedUploadFiles', persistedUploadFiles);
-
-
-
-
diff --git a/ui/src/hooks/useFileOperation.ts b/ui/src/hooks/useFileOperation.ts
new file mode 100644
index 00000000..e363ca21
--- /dev/null
+++ b/ui/src/hooks/useFileOperation.ts
@@ -0,0 +1,335 @@
+import type { ConfigProviderProps } from 'naive-ui';
+
+import { v4 as uuid } from 'uuid';
+import { useI18n } from 'vue-i18n';
+import { computed, ref } from 'vue';
+import { useWebSocket } from '@vueuse/core';
+import { createDiscreteApi, darkTheme } from 'naive-ui';
+
+import type { COMMAND_TYPE } from '@/types/modules/message.type';
+import type { FileManage, FileManageSftpFileItem } from '@/types/modules/file.type';
+
+import { createDownloadLink } from '@/utils';
+import { BASE_WS_URL } from '@/utils/config';
+import { FILE_MANAGE_MESSAGE_TYPE, SFTP_CMD } from '@/types/modules/message.type';
+
+interface sendBody {
+ id: string;
+ cmd: string;
+ type: string;
+ data: string;
+}
+
+interface sendDataArgs {
+ path?: string;
+ id?: string;
+ filename?: string;
+ cmd?: SFTP_CMD;
+ type?: FILE_MANAGE_MESSAGE_TYPE;
+ sendData?: Record;
+}
+
+type CommandHandler = (message: any) => void;
+type CommandMap = Record;
+
+const OK = 'ok';
+const DENIED = 'permission denied';
+
+const configProviderPropsRef = computed(() => ({
+ theme: darkTheme,
+}));
+
+export const useFileOperation = () => {
+ const { t } = useI18n();
+
+ const messageId = ref('');
+ const initialPath = ref('');
+ const currentPath = ref('');
+ const fileSocket = ref(null);
+ const receivedBuffers = ref([]);
+ const fileList = ref([]);
+
+ const { message, notification } = createDiscreteApi(['message', 'notification'], {
+ configProviderProps: configProviderPropsRef,
+ });
+
+ const commandsDispatch: CommandMap = {
+ rm(msg) {
+ if (isOk(msg.data)) return opOk();
+ if (isDenied(msg.err)) return opDenied();
+ },
+
+ list(msg) {
+ try {
+ let files: FileManageSftpFileItem[] = [];
+
+ if (typeof msg.data === 'string' && msg.data) {
+ files = JSON.parse(msg.data);
+ }
+
+ // 第一次拿到列表时,记录初始路径
+ if (!initialPath.value) {
+ initialPath.value = currentPath.value || '/';
+ }
+
+ // 只有当不在根路径或初始路径时,才注入 '..'
+ const needParent = currentPath.value && currentPath.value !== '/' && currentPath.value !== initialPath.value;
+
+ if (needParent) {
+ const hasParent = files.length && files[0]?.name === '..';
+
+ if (!hasParent) {
+ files = [
+ {
+ name: '..',
+ size: '',
+ perm: '',
+ mod_time: '',
+ type: '',
+ is_dir: true,
+ } as FileManageSftpFileItem,
+ ...files,
+ ];
+ }
+ }
+
+ fileList.value = files;
+ } catch (_) {
+ message.error(t('FileListError'));
+ fileList.value = [];
+ }
+ },
+
+ mkdir(msg) {
+ if (isOk(msg.data)) return opOk();
+ },
+
+ rename(msg) {
+ if (isOk(msg.data)) return opOk();
+ },
+
+ upload(msg) {
+ if (isOk(msg.data)) return opOk('UploadSuccess');
+ if (isDenied(msg.err)) return opDenied('UploadFailed');
+ },
+
+ download(msg) {
+ if (msg.data) return createDownloadLink(receivedBuffers.value, message);
+ if (isDenied(msg.err)) return opDenied('DownloadFailed');
+ },
+ };
+
+ function isOk(s?: string | null) {
+ return (s ?? '').toLowerCase() === OK;
+ }
+
+ function isDenied(s?: string | null) {
+ return (s ?? '').toLowerCase() === DENIED;
+ }
+
+ function opOk(tipMessage?: string) {
+ message.success(t(tipMessage ?? 'OperationSuccessful'));
+ }
+
+ function opDenied(tipMessage?: string) {
+ message.error(t(tipMessage ?? 'PermissionDenied'));
+ }
+
+ /**
+ * @description 构造消息体
+ */
+ const createSftpMessage = ({
+ path = '',
+ id = uuid(),
+ cmd = SFTP_CMD.LIST,
+ type = FILE_MANAGE_MESSAGE_TYPE.SFTP_DATA,
+ sendData = {},
+ }: sendDataArgs): sendBody => {
+ const payload = {
+ path,
+ ...sendData,
+ };
+
+ const sendBody: sendBody = {
+ id,
+ cmd,
+ type,
+ data: JSON.stringify(payload),
+ };
+
+ return sendBody;
+ };
+
+ /**
+ * @description 重命名
+ */
+ const handleFileRename = (path: string, filename: string) => {
+ const sendBody = createSftpMessage({
+ id: uuid(),
+ path,
+ cmd: SFTP_CMD.RENAME,
+ sendData: {
+ new_name: filename,
+ },
+ });
+
+ fileSocket.value?.send(JSON.stringify(sendBody));
+ };
+
+ /**
+ * @description 移除文件
+ */
+ const handleFileRemove = (path: string) => {
+ const sendBody = createSftpMessage({
+ id: uuid(),
+ path,
+ cmd: SFTP_CMD.RM,
+ });
+
+ fileSocket.value?.send(JSON.stringify(sendBody));
+ };
+
+ /**
+ * @description 下载文件
+ */
+ const handleFileDownload = () => {};
+
+ /**
+ * @description 创建文件夹
+ */
+ const handleCreateFolder = (path: string) => {
+ const sendBody = createSftpMessage({
+ id: uuid(),
+ path,
+ cmd: SFTP_CMD.MKDIR,
+ });
+
+ fileSocket.value?.send(JSON.stringify(sendBody));
+ };
+
+ /**
+ * @description 刷新文件列表
+ */
+ const handleRefreshFileList = (path: string) => {
+ const sendBody = createSftpMessage({
+ id: uuid(),
+ path,
+ });
+
+ fileSocket.value?.send(JSON.stringify(sendBody));
+ };
+
+ /**
+ * @description 处理文件消息
+ * @param {MessageEvent} event
+ */
+ const handleFileMessage = (event: MessageEvent) => {
+ const messageData: FileManage = JSON.parse(event.data);
+
+ // fileManageStore.setMessageId(message.id);
+ // fileManageStore.setCurrentPath(message.current_path);
+ messageId.value = messageData.id;
+ currentPath.value = messageData.current_path;
+
+ switch (messageData.type) {
+ case FILE_MANAGE_MESSAGE_TYPE.CONNECT: {
+ const sendBody = createSftpMessage({
+ id: messageData.id,
+ });
+
+ fileSocket.value?.send(JSON.stringify(sendBody));
+
+ break;
+ }
+
+ case FILE_MANAGE_MESSAGE_TYPE.CLOSE: {
+ break;
+ }
+
+ case FILE_MANAGE_MESSAGE_TYPE.ERROR: {
+ message.error(messageData.err ? messageData.err : t('FileListError'));
+ break;
+ }
+
+ case FILE_MANAGE_MESSAGE_TYPE.PING: {
+ fileSocket.value?.send(
+ JSON.stringify({
+ id: uuid(),
+ type: FILE_MANAGE_MESSAGE_TYPE.PONG,
+ data: 'pong',
+ })
+ );
+ break;
+ }
+
+ case FILE_MANAGE_MESSAGE_TYPE.PONG: {
+ break;
+ }
+
+ case FILE_MANAGE_MESSAGE_TYPE.SFTP_DATA: {
+ const handler = commandsDispatch[messageData.cmd as keyof typeof commandsDispatch];
+
+ if (handler) {
+ handler(messageData);
+ }
+ break;
+ }
+
+ // 在 SFTP_BINARY 中只会传输数据进行,真正的下载行为需要等到 SFTP_DATA 的 download
+ case FILE_MANAGE_MESSAGE_TYPE.SFTP_BINARY: {
+ try {
+ const binaryString = atob(messageData.raw);
+ const bytes = Uint8Array.from(binaryString, c => c.charCodeAt(0));
+
+ receivedBuffers.value.push(bytes);
+ } catch (_) {
+ // TODO 翻译
+ message.error(t('SFTP_BINARY 解码失败'));
+ }
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ };
+
+ /**
+ * @description 创建文件 socket
+ */
+ const createFileSocket = (token: string) => {
+ const connectionUrl = `${BASE_WS_URL}/koko/ws/sftp/?token=${token}`;
+
+ const { ws } = useWebSocket(connectionUrl, {
+ protocols: ['JMS-KOKO'],
+ autoReconnect: {
+ retries: 5,
+ delay: 3000,
+ },
+ });
+
+ if (!ws.value) return message.error(t('FileListError'));
+
+ ws.value.binaryType = 'arraybuffer';
+ ws.value.close = () => {};
+ ws.value.onopen = () => {};
+ ws.value.onmessage = (event: MessageEvent) => handleFileMessage(event);
+
+ fileSocket.value = ws.value;
+ };
+
+ return {
+ fileList,
+ fileSocket,
+ currentPath,
+ initialPath,
+
+ createFileSocket,
+ handleFileRename,
+ handleFileRemove,
+ handleFileDownload,
+ handleCreateFolder,
+ handleRefreshFileList,
+ };
+};
diff --git a/ui/src/types/modules/file.type.ts b/ui/src/types/modules/file.type.ts
index 1d8717a9..bfbb1e27 100644
--- a/ui/src/types/modules/file.type.ts
+++ b/ui/src/types/modules/file.type.ts
@@ -126,3 +126,4 @@ export interface FileSendData {
merge?: boolean;
chunk?: boolean;
}
+
diff --git a/ui/src/types/modules/message.type.ts b/ui/src/types/modules/message.type.ts
index 27704909..57bcd602 100644
--- a/ui/src/types/modules/message.type.ts
+++ b/ui/src/types/modules/message.type.ts
@@ -81,6 +81,8 @@ export enum SFTP_CMD {
DOWNLOAD = 'download',
}
+export type COMMAND_TYPE = 'mkdir' | 'rm' | 'rename' | 'upload' | 'download' | 'list';
+
export enum FILE_MANAGE_MESSAGE_TYPE {
CONNECT = 'CONNECT',
CLOSE = 'CLOSE',
diff --git a/ui/src/utils/index.ts b/ui/src/utils/index.ts
index 213f61e0..d47d4cfc 100644
--- a/ui/src/utils/index.ts
+++ b/ui/src/utils/index.ts
@@ -116,3 +116,27 @@ export function formatMessage(id: string, type: string, data: any) {
data,
});
}
+
+/**
+ * @description 创建下载链接
+ * @param buffer
+ * @param message
+ */
+export function createDownloadLink(buffer: ArrayBuffer[], message: any) {
+ const blob: Blob = new Blob(buffer, {
+ type: 'application/octet-stream',
+ });
+
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+
+ a.style.display = 'none';
+ a.href = url;
+ a.download = message.data;
+
+ document.body.appendChild(a);
+ a.click();
+
+ window.URL.revokeObjectURL(url);
+ document.body.removeChild(a);
+}