From 286dd8e3862b1a25efaa2db2520c18502bcc442b Mon Sep 17 00:00:00 2001 From: Miao_Spring <539943419@qq.com> Date: Mon, 17 Mar 2025 20:56:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84VirtualFile=E5=92=8CV?= =?UTF-8?q?irtualDirectory=E7=9A=84=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/class/VirtualDirectory.ts | 192 ++++++++------------ src/class/VirtualRemoteDirectory.ts | 185 +++++++++++++++++++ src/hooks/useRootVDirectory.ts | 4 +- src/plugins/miaoDirectory/miaoDirectory.vue | 71 ++------ 4 files changed, 284 insertions(+), 168 deletions(-) create mode 100644 src/class/VirtualRemoteDirectory.ts diff --git a/src/class/VirtualDirectory.ts b/src/class/VirtualDirectory.ts index de35c03..4446bfa 100644 --- a/src/class/VirtualDirectory.ts +++ b/src/class/VirtualDirectory.ts @@ -1,10 +1,6 @@ -import config from '@/config' import generateId from '@/utils/generateId' -import useMiaoFetchApi from '@/hooks/useMiaoFetchApi' import type { stats, file, directory } from '@/types/type.ts' -import difference from 'lodash/difference' -const miaoFetchApi = useMiaoFetchApi() // const vDirectoryMap = new Map() // const vFileMap = new Map() @@ -24,65 +20,63 @@ export interface Tree { directories: Tree[] } -export class VirtualFile { - constructor(info: file, parent: VirtualDirectory) { - this.name = info.name - this.size = info.size - this.stats = info.stats - this.parent = parent - this.type = 'file' - this.id = generateId() - // if (vFileMap.get(this.path) !== undefined) { - // throw `${this.path} is already exist` - // } - // vFileMap.set(this.path, this) - } +export abstract class VirtualFileBase { name: string - type: 'file' + type: 'file' = 'file' size: number stats: stats - parent: VirtualDirectory + parent: VirtualDirectoryBase id: number storage: { [key: string]: any } = {} + + constructor(name: string, size: number, stats: stats, parent: VirtualDirectoryBase) { + this.name = name + this.size = size + this.stats = stats + this.parent = parent + this.id = generateId() + } + get path() { return `${this.parent.path}${this.name}` } - get url() { - return `${config.api.get}${this.parent.path}${this.name}` - } - - // destroy() { - // vFileMap.delete(this.path) - // } + abstract get url(): string; + + /** + * 删除文件 + * @returns {Promise} 删除是否成功 + */ + abstract delete(): Promise; + + /** + * 重命名文件 + * @param {string} newName 新文件名 + * @returns {Promise} 重命名是否成功 + */ + abstract rename(newName: string): Promise; + + /** + * 移动文件到目标目录 + * @param {VirtualDirectoryBase} targetDir 目标目录 + * @returns {Promise} 移动是否成功 + */ + abstract moveTo(targetDir: VirtualDirectoryBase): Promise; } -class VirtualDirectory { - /** - * 除非为根节点,否则必须设置parent - */ - constructor(info: directory, parent?: VirtualDirectory) { - this.name = info.name - this.stats = info.stats - this.parent = parent ?? undefined - this.id = generateId() - // if (vDirectoryMap.get(this.path) !== undefined) { - // throw `${this.path} is already exist` - // } - // vDirectoryMap.set(this.path, this) - } +export abstract class VirtualDirectoryBase { // 文件夹名 name: string // 子文件 - files: VirtualFile[] = [] + files: VirtualFileBase[] = [] // 子目录 - directories: VirtualDirectory[] = [] + directories: VirtualDirectoryBase[] = [] // 没啥意义,标注一下就是了 type: 'directory' = 'directory' // 文件夹创建时间等杂项属性 stats: stats // 父文件夹 - parent?: VirtualDirectory + parent?: VirtualDirectoryBase _isUpdated: boolean = false @@ -91,6 +85,13 @@ class VirtualDirectory { // 用来存储一些标记信息啥的 storage: { [key: string]: any } = {} + constructor(name: string, stats: stats, parent?: VirtualDirectoryBase) { + this.name = name + this.stats = stats + this.parent = parent + this.id = generateId() + } + /** * 获取相对于根目录的路径的层数 * 用于目录下文件定位 @@ -99,6 +100,7 @@ class VirtualDirectory { get layer(): number { return this.parent ? this.parent.layer + 1 : 0 } + /** * 相对于根目录的路径, 如: "/", "/123/321/" * 用于目录下文件定位 @@ -111,15 +113,13 @@ class VirtualDirectory { return `${this.parent.path}${this.name}/` } - get url(): string { - return `${config.api.get}${this.path}/` - } + abstract get url(): string; /** * 获取从根目录开始的按顺序的文件夹 - * @returns {VirtualDirectory[]} + * @returns {VirtualDirectoryBase[]} */ - get getParents(): VirtualDirectory[] { + get getParents(): VirtualDirectoryBase[] { if (!this.parent) { return [this] } @@ -136,9 +136,9 @@ class VirtualDirectory { /** * 获取 layer 级父文件夹, 如getParent(1)就返回父级 * @param {number} layer 需要跳转的层数 - * @returns {VirtualDirectory} + * @returns {VirtualDirectoryBase} */ - getParent(layer: number): VirtualDirectory { + getParent(layer: number): VirtualDirectoryBase { if (layer <= 0 || !this.parent) { return this } @@ -163,77 +163,39 @@ class VirtualDirectory { } } - // 时间复杂度大爆炸 - updateContent(content: (file | directory)[]) { - this._isUpdated = true - - const itemNames: string[] = content.map((v) => v.name) - // 删去远程端已不存在的文件与文件夹 - const _delete = (target: (VirtualFile | VirtualDirectory)[]) => { - let index = 0 - while (index < target.length) { - if (!itemNames.includes(target[index].name)) { - target.splice(index, 1) - } else { - index += 1 - } - } - } - const _addFile = (item: file) => { - const nvf = - // vFileMap.get(`${this.path}${item.name}`) ?? - new VirtualFile(item, this) - this.files.push(nvf) - } - const _addDirectory = (item: directory) => { - const nvd = - // vDirectoryMap.get(`${this.path}${item.name}/`) ?? - new VirtualDirectory(item, this) - this.directories.push(nvd) - } - const newFileNames: string[] = difference( - content.filter((v) => v.type === 'file').map((v) => v.name), - this.files.map((v) => v.name) - ) - const newDirectoryNames: string[] = difference( - content.filter((v) => v.type === 'directory').map((v) => v.name), - this.directories.map((v) => v.name) - ) - _delete(this.files) - _delete(this.directories) - // 加入新的文件与文件夹 - for (let item of content.filter((v) => newFileNames.includes(v.name))) { - _addFile(item as file) - } - for (let item of content.filter((v) => - newDirectoryNames.includes(v.name) - )) { - _addDirectory(item as directory) - } - } - - hasChild(item: VirtualDirectory | VirtualFile): boolean { + hasChild(item: VirtualDirectoryBase | VirtualFileBase): boolean { if (item.type === 'directory') { - return this.directories?.includes(item) ?? false + return this.directories?.includes(item as VirtualDirectoryBase) ?? false } else { - return this.files?.includes(item) ?? false + return this.files?.includes(item as VirtualFileBase) ?? false } } - // destroy() { - // for(const item of [...this.files, ...this.directories]){ - // item.destroy() - // } - // vDirectoryMap.delete(this.path) - // } - + abstract updateContent(content: (file | directory)[]): void; + abstract update(): Promise; + /** - * 调用后重新向远端拉取文件夹信息并进行更新 - * @returns {void} + * 删除目录 + * @returns {Promise} 删除是否成功 */ - async update(): Promise { - const data: (file | directory)[] = await miaoFetchApi.get(this) - this.updateContent(data) - } + abstract delete(): Promise; + + /** + * 重命名目录 + * @param {string} newName 新目录名 + * @returns {Promise} 重命名是否成功 + */ + abstract rename(newName: string): Promise; + + /** + * 移动目录到目标目录 + * @param {VirtualDirectoryBase} targetDir 目标目录 + * @returns {Promise} 移动是否成功 + */ + abstract mv(targetDir: VirtualDirectoryBase): Promise; } + +export abstract class VirtualFile extends VirtualFileBase {} +export abstract class VirtualDirectory extends VirtualDirectoryBase {} + export default VirtualDirectory diff --git a/src/class/VirtualRemoteDirectory.ts b/src/class/VirtualRemoteDirectory.ts new file mode 100644 index 0000000..61121b8 --- /dev/null +++ b/src/class/VirtualRemoteDirectory.ts @@ -0,0 +1,185 @@ +import { file, directory } from "@/types/type"; +import { VirtualFileBase, VirtualDirectoryBase } from "./VirtualDirectory"; +import useMiaoFetchApi from "@/hooks/useMiaoFetchApi"; +import config from '@/config' + +const miaoFetchApi = useMiaoFetchApi() + +export class VirtualRemoteFile extends VirtualFileBase { + constructor(info: file, parent: VirtualDirectoryBase) { + super(info.name, info.size, info.stats, parent) + } + + get url() { + return `${config.api.get}${this.parent.path}${this.name}` + } + + async delete(): Promise { + try { + const { response } = miaoFetchApi.delete(this, { + retry: 5 + }); + const id = (await response).eventId; + const { response: res } = miaoFetchApi.query(id); + const eventResult = await res; + return eventResult.status === 'success'; + } catch (error) { + console.error('删除文件失败:', error); + return false; + } + } + + async rename(newName: string): Promise { + if (!newName) { + return false; + } + + try { + const { response } = miaoFetchApi.rename(this, newName, { + retry: 5 + }); + const id = (await response).eventId; + const { response: res } = miaoFetchApi.query(id); + const eventResult = await res; + return eventResult.status === 'success'; + } catch (error) { + console.error('重命名文件失败:', error); + return false; + } + } + + async moveTo(targetDir: VirtualDirectoryBase): Promise { + if (this.parent === targetDir) { + return false; // 已经在目标目录中 + } + + try { + const _from = this.parent; + const { response } = miaoFetchApi.cut(this, targetDir); + const id = (await response).eventId; + const { response: res } = miaoFetchApi.query(id); + const eventResult = await res; + const success = eventResult.status === 'success'; + + if (success) { + _from.update(); + } + + return success; + } catch (error) { + console.error('移动文件失败:', error); + return false; + } + } +} + +export class VirtualRemoteDirectory extends VirtualDirectoryBase { + constructor(info: directory, parent?: VirtualDirectoryBase) { + super(info.name, info.stats, parent) + } + + get url(): string { + return `${config.api.get}${this.path}/` + } + + updateContent(content: (file | directory)[]) { + this._isUpdated = true + + // 创建名称到内容项的映射,O(n) + const contentMap = new Map(); + content.forEach(item => contentMap.set(item.name, item)); + + // 使用Set存储所有内容项的名称,用于O(1)查找,O(n) + const itemNamesSet = new Set(content.map(v => v.name)); + + // 删除不再存在的文件和目录,O(m) + this.files = this.files.filter(file => itemNamesSet.has(file.name)); + this.directories = this.directories.filter(dir => itemNamesSet.has(dir.name)); + + // 创建现有文件和目录的名称集合,O(m) + const existingFileNames = new Set(this.files.map(file => file.name)); + const existingDirNames = new Set(this.directories.map(dir => dir.name)); + + // 添加新文件和目录,O(n) + content.forEach(item => { + // 如果是文件且不存在于现有文件中 + if (item.type === 'file' && !existingFileNames.has(item.name)) { + const nvf = new VirtualRemoteFile(item as file, this); + this.files.push(nvf); + } + // 如果是目录且不存在于现有目录中 + else if (item.type === 'directory' && !existingDirNames.has(item.name)) { + const nvd = new VirtualRemoteDirectory(item as directory, this); + this.directories.push(nvd); + } + }); + } + + async update(): Promise { + const data: (file | directory)[] = await miaoFetchApi.get(this) + this.updateContent(data) + } + + async delete(): Promise { + try { + const { response } = miaoFetchApi.delete(this, { + retry: 5 + }); + const id = (await response).eventId; + const { response: res } = miaoFetchApi.query(id); + const eventResult = await res; + return eventResult.status === 'success'; + } catch (error) { + console.error('删除目录失败:', error); + return false; + } + } + + async rename(newName: string): Promise { + if (!newName) { + return false; + } + + try { + const { response } = miaoFetchApi.rename(this, newName, { + retry: 5 + }); + const id = (await response).eventId; + const { response: res } = miaoFetchApi.query(id); + const eventResult = await res; + return eventResult.status === 'success'; + } catch (error) { + console.error('重命名目录失败:', error); + return false; + } + } + + async mv(targetDir: VirtualDirectoryBase): Promise { + // 不能移动到自己或自己的子目录 + if (targetDir === this || this.getParents.includes(targetDir as VirtualDirectoryBase)) { + return false; + } + + if (this.parent === targetDir) { + return false; // 已经在目标目录中 + } + + try { + const _from = this.parent; + const { response } = miaoFetchApi.cut(this, targetDir); + const id = (await response).eventId; + const { response: res } = miaoFetchApi.query(id); + const eventResult = await res; + const success = eventResult.status === 'success'; + + if (success && _from) { + _from.update(); + } + + return success; + } catch (error) { + console.error('移动目录失败:', error); + return false; + } + } +} diff --git a/src/hooks/useRootVDirectory.ts b/src/hooks/useRootVDirectory.ts index 3e9dd83..76cdd26 100644 --- a/src/hooks/useRootVDirectory.ts +++ b/src/hooks/useRootVDirectory.ts @@ -1,8 +1,8 @@ -import VirtualDirectory from '@/class/VirtualDirectory' +import { VirtualRemoteDirectory } from '@/class/VirtualRemoteDirectory' import { reactive } from 'vue' const root = reactive( - new VirtualDirectory({ + new VirtualRemoteDirectory({ name: '根目录', stats: { atimeMs: 0, diff --git a/src/plugins/miaoDirectory/miaoDirectory.vue b/src/plugins/miaoDirectory/miaoDirectory.vue index dc44668..7dba531 100644 --- a/src/plugins/miaoDirectory/miaoDirectory.vue +++ b/src/plugins/miaoDirectory/miaoDirectory.vue @@ -441,18 +441,12 @@ const handleItemDownload = (item: VirtualFile) => { const handleItemDelete = async (_item: VirtualDirectory | VirtualFile) => { for (let item of uniq([...selectedItem.value, _item])) { - ;(async () => { - const { response } = miaoFetchApi.delete(item, { - retry: 5 - }) - // todo: 将删除事件统一用一个事件管理中心管理 - const id = (await response).eventId - const { response: res } = miaoFetchApi.query(id) - const eventResult = await res - if (eventResult.status === 'success') { - reload() + (async () => { + const success = await item.delete(); + if (success) { + reload(); } - })() + })(); } } @@ -483,14 +477,10 @@ const handleItemRename = async (item: VirtualDirectory | VirtualFile) => { if (!newName) { return } - const { response } = miaoFetchApi.rename(item, newName, { - retry: 5 - }) - const id = (await response).eventId - const { response: res } = miaoFetchApi.query(id) - const eventResult = await res - if (eventResult.status === 'success') { - reload() + + const success = await item.rename(newName); + if (success) { + reload(); } } @@ -520,46 +510,25 @@ const handleDropFiles = async (files: File[]) => { } const handleDropVDirectory = async (vDirs: VirtualDirectory[]) => { - // 不能移到自己或自己的子文件夹内 - for (let vDir of vDirs) { - if (currentDirectory.value.getParents.includes(vDir)) { - return - } - if (vDir.parent === currentDirectory.value) { - return - } - } + // 不能移到自己或自己的子文件夹内 - 这部分逻辑已经移到类的实现中 for (let dir of vDirs) { - ;(async () => { - const _from = dir.parent - const { response } = miaoFetchApi.cut(dir, currentDirectory.value) - const id = (await response).eventId - const { response: res } = miaoFetchApi.query(id) - const eventResult = await res - if (eventResult.status === 'success') { - reload() - _from && _from.update() + (async () => { + const success = await dir.mv(currentDirectory.value); + if (success) { + reload(); } - })() + })(); } } const handleDropVirtualFiles = (files: VirtualFile[]) => { - if (files[0].parent === currentDirectory.value) { - return - } for (let file of files) { - ;(async () => { - const _from = file.parent - const { response } = miaoFetchApi.cut(file, currentDirectory.value) - const id = (await response).eventId - const { response: res } = miaoFetchApi.query(id) - const eventResult = await res - if (eventResult.status === 'success') { - reload() - _from.update() + (async () => { + const success = await file.moveTo(currentDirectory.value); + if (success) { + reload(); } - })() + })(); } }