Private
Public Access
1
0

重构:文件系统模块化架构,优化应用启动流程

This commit is contained in:
2026-01-28 00:28:54 +08:00
parent 4a9b25a505
commit 8c577f70e7
123 changed files with 32030 additions and 967 deletions

View File

@@ -0,0 +1,272 @@
/**
* 收藏夹管理 composable
*
* @module composables/useFavoriteFiles
* @description 封装收藏夹的增删改查逻辑,支持持久化存储
*/
import { ref, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { useLocalStorage } from './useLocalStorage'
/**
* 收藏夹 composable
* @param {string} storageKey - localStorage 键名
* @param {Object} [options] - 配置选项
* @param {number} [options.maxLength=50] - 最大收藏数量
* @param {Function} [options.onAdd] - 添加收藏回调
* @param {Function} [options.onRemove] - 移除收藏回调
* @returns {UseFavoriteFilesReturn} 收藏夹操作API
*
* @example
* const {
* favoriteFiles,
* isFavorite,
* toggleFavorite,
* removeFavorite,
* clearAll
* } = useFavoriteFiles('app-favorites')
*
* // 在模板中使用
* <a-button @click="toggleFavorite(file)">
* <icon-star-fill v-if="isFavorite(file.path)" />
* <icon-star v-else />
* </a-button>
*/
export function useFavoriteFiles(storageKey, options = {}) {
const {
maxLength = 50,
onAdd = () => {},
onRemove = () => {},
} = options
// 使用 localStorage composable 管理收藏列表
const { storedValue: favoriteFiles, load, save } = useLocalStorage(
storageKey,
[]
)
/**
* 判断文件/目录是否已收藏
* @param {string} path - 文件/目录路径
* @returns {boolean} 是否已收藏
*/
const isFavorite = (path) => {
if (!path || !Array.isArray(favoriteFiles.value)) {
return false
}
return favoriteFiles.value.some(fav => fav.path === path)
}
/**
* 切换收藏状态
* @param {Object} item - 文件/目录信息
* @param {string} item.path - 路径
* @param {string} item.name - 名称
* @param {boolean} item.is_dir - 是否为目录
* @returns {boolean} 操作后是否为收藏状态
*/
const toggleFavorite = (item) => {
if (!item || !item.path) {
Message.warning('无效的文件信息')
return false
}
const index = favoriteFiles.value.findIndex(fav => fav.path === item.path)
if (index > -1) {
// 已收藏,执行取消收藏
favoriteFiles.value.splice(index, 1)
save(favoriteFiles.value)
onRemove(item)
Message.info(`已取消收藏: ${item.name}`)
return false
} else {
// 未收藏,执行添加收藏
if (favoriteFiles.value.length >= maxLength) {
Message.warning(`收藏夹已满,最多只能收藏 ${maxLength}`)
return false
}
favoriteFiles.value.push({
path: item.path,
name: item.name,
is_dir: item.is_dir || false,
created_at: Date.now(), // 添加时间戳
})
save(favoriteFiles.value)
onAdd(item)
Message.success(`已收藏: ${item.name}`)
return true
}
}
/**
* 移除收藏
* @param {string} path - 文件/目录路径
* @returns {boolean} 是否成功移除
*/
const removeFavorite = (path) => {
if (!path) {
Message.warning('请提供有效的路径')
return false
}
const index = favoriteFiles.value.findIndex(fav => fav.path === path)
if (index === -1) {
Message.warning('该路径不在收藏夹中')
return false
}
const item = favoriteFiles.value[index]
favoriteFiles.value.splice(index, 1)
save(favoriteFiles.value)
onRemove(item)
Message.info(`已取消收藏: ${item.name}`)
return true
}
/**
* 打开收藏的文件/目录
* @param {string} path - 文件/目录路径
* @param {Function} onOpen - 打开回调函数
* @returns {Promise<boolean>} 是否成功打开
*/
const openFavorite = async (path, onOpen) => {
if (!path || !onOpen) {
return false
}
const item = favoriteFiles.value.find(fav => fav.path === path)
if (!item) {
Message.warning('收藏项不存在')
return false
}
return await onOpen(item)
}
/**
* 清空所有收藏
* @param {boolean} [confirm=true] - 是否需要确认
* @returns {boolean} 是否成功清空
*/
const clearAll = (confirm = true) => {
const executeClear = () => {
const count = favoriteFiles.value.length
favoriteFiles.value = []
save([])
Message.success(`已清空 ${count} 个收藏项`)
return true
}
if (!confirm) {
return executeClear()
}
// 使用原生 confirm简单场景
if (window.confirm(`确定要清空所有 ${favoriteFiles.value.length} 个收藏项吗?`)) {
return executeClear()
}
return false
}
/**
* 获取收藏列表(按创建时间排序)
* @param {string} [order='desc'] - 排序方式:'asc'或'desc'
* @returns {Array} 排序后的收藏列表
*/
const getSortedFavorites = (order = 'desc') => {
const sorted = [...favoriteFiles.value]
sorted.sort((a, b) => {
const timeA = a.created_at || 0
const timeB = b.created_at || 0
return order === 'desc' ? timeB - timeA : timeA - timeB
})
return sorted
}
/**
* 按名称搜索收藏
* @param {string} keyword - 搜索关键词
* @returns {Array} 匹配的收藏列表
*/
const searchFavorites = (keyword) => {
if (!keyword || !keyword.trim()) {
return favoriteFiles.value
}
const lowerKeyword = keyword.toLowerCase().trim()
return favoriteFiles.value.filter(fav =>
fav.name.toLowerCase().includes(lowerKeyword) ||
fav.path.toLowerCase().includes(lowerKeyword)
)
}
// 组件挂载时加载数据
onMounted(() => {
load()
})
return {
// 状态
favoriteFiles,
// 方法
isFavorite,
toggleFavorite,
removeFavorite,
openFavorite,
clearAll,
getSortedFavorites,
searchFavorites,
load,
save,
}
}
/**
* @typedef {Object} UseFavoriteFilesReturn
* @property {Ref<Array>} favoriteFiles - 收藏列表
* @property {Function} isFavorite - 判断是否已收藏
* @property {Function} toggleFavorite - 切换收藏状态
* @property {Function} removeFavorite - 移除收藏
* @property {Function} openFavorite - 打开收藏项
* @property {Function} clearAll - 清空所有收藏
* @property {Function} getSortedFavorites - 获取排序后的列表
* @property {Function} searchFavorites - 搜索收藏
* @property {Function} load - 手动加载数据
* @property {Function} save - 手动保存数据
*/
/**
* 创建多个收藏夹管理实例
* @param {Object} config - 配置对象
* @returns {Object} 收藏夹管理实例集合
*
* @example
* const {
* filesystemFavs,
* deviceTestFavs
* } = createMultipleFavorites({
* filesystem: 'app-filesystem-favorites',
* deviceTest: 'app-device-test-favorites'
* })
*/
export function createMultipleFavorites(config) {
const result = {}
Object.keys(config).forEach(key => {
result[key] = useFavoriteFiles(config[key])
})
return result
}

View File

@@ -0,0 +1,372 @@
/**
* 文件操作逻辑封装
*
* @module composables/useFileOperations
* @description 封装所有文件操作逻辑,提供统一的错误处理和状态管理
*/
import { ref, watch } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import {
listDir,
readFile as readFileApi,
writeFile as writeFileApi,
deletePath as deletePathApi,
} from '@/api'
/**
* LocalStorage 键名
*/
const STORAGE_KEY_LAST_PATH = 'app-filesystem-last-path'
/**
* 文件操作 composable
* @param {Object} [options] - 配置选项
* @param {Function} [options.onSuccess] - 操作成功回调
* @param {Function} [options.onError] - 操作失败回调
* @returns {UseFileOperationsReturn} 文件操作API
*
* @example
* const {
* filePath,
* fileContent,
* fileList,
* fileLoading,
* listDirectory,
* readFile,
* writeFile,
* deleteFile
* } = useFileOperations({
* onSuccess: (operation, data) => console.log(operation, data),
* onError: (operation, error) => console.error(operation, error)
* })
*/
export function useFileOperations(options = {}) {
const { onSuccess = () => {}, onError = () => {} } = options
// ========== 响应式状态 ==========
/**
* 当前文件/目录路径
* @type {Ref<string>}
*/
// 从 localStorage 恢复上次路径
const savedPath = localStorage.getItem(STORAGE_KEY_LAST_PATH)
const filePath = ref(savedPath || '')
/**
* 文件内容
* @type {Ref<string>}
*/
const fileContent = ref('')
/**
* 文件列表
* @type {Ref<Array>}
*/
const fileList = ref([])
/**
* 加载状态
* @type {Ref<boolean>}
*/
const fileLoading = ref(false)
/**
* 正在删除状态(防止并发删除)
* @type {Ref<boolean>}
*/
const isDeleting = ref(false)
// ========== 文件操作方法 ==========
/**
* 列出目录内容
* @param {string} [path] - 目录路径,不传则使用当前路径
* @returns {Promise<boolean>} 是否成功
*/
const listDirectory = async (path) => {
const targetPath = path || filePath.value
if (!targetPath) {
Message.error('请输入目录路径')
return false
}
fileLoading.value = true
try {
const result = await listDir(targetPath)
fileList.value = result
if (!path) {
// 如果没有传参,更新当前路径
filePath.value = targetPath
}
onSuccess('listDirectory', { path: targetPath, count: result.length })
return true
} catch (error) {
onError('listDirectory', error)
Message.error(`列出目录失败: ${error.message || error}`)
return false
} finally {
fileLoading.value = false
}
}
/**
* 读取文件内容
* @param {string} [path] - 文件路径,不传则使用当前路径
* @returns {Promise<boolean>} 是否成功
*/
const readFile = async (path) => {
const targetPath = path || filePath.value
if (!targetPath) {
Message.error('请输入文件路径')
return false
}
fileLoading.value = true
try {
const content = await readFileApi(targetPath)
fileContent.value = content
if (!path) {
filePath.value = targetPath
}
onSuccess('readFile', { path: targetPath })
// 文件读取成功,静默无提示
return true
} catch (error) {
onError('readFile', error)
Message.error(`读取文件失败: ${error.message || error}`)
return false
} finally {
fileLoading.value = false
}
}
/**
* 写入文件内容
* @param {string} [content] - 要写入的内容,不传则使用当前内容
* @param {string} [path] - 文件路径,不传则使用当前路径
* @param {string} [fileName] - 文件名,用于成功提示显示
* @param {boolean} [isShortcut=false] - 是否是快捷键触发(快捷键不显示提示)
* @returns {Promise<boolean>} 是否成功
*/
const writeFile = async (content, path, fileName, isShortcut = false) => {
// 忽略事件对象(当点击按钮时 Vue 会传递事件对象)
const targetContent = (content !== undefined && typeof content === 'string') ? content : fileContent.value
const targetPath = (path !== undefined && typeof path === 'string') ? path : filePath.value
if (!targetPath) {
Message.error('请输入文件路径')
return false
}
fileLoading.value = true
try {
await writeFileApi(targetPath, targetContent)
if (content !== undefined) {
fileContent.value = targetContent
}
if (path) {
filePath.value = path
}
onSuccess('writeFile', { path: targetPath })
// 差异化反馈:快捷键不显示提示,按钮点击显示轻提示
if (!isShortcut) {
// 按钮点击保存:显示轻量 Toast 提示
if (fileName && typeof fileName === 'string') {
Message.success({
content: `${fileName} 已保存`,
duration: 1500, // 1.5秒后自动消失
position: 'bottom' // 底部显示,不打断操作
})
} else {
Message.success({
content: '文件已保存',
duration: 1500,
position: 'bottom'
})
}
}
// 快捷键保存:无提示(静默成功)
return true
} catch (error) {
onError('writeFile', error)
// 保存失败:总是显示醒目错误提示(需手动关闭)
Message.error({
content: `文件保存失败: ${error.message || error}`,
duration: 5000, // 错误提示显示更久
closable: true // 允许手动关闭
})
return false
} finally {
fileLoading.value = false
}
}
/**
* 删除文件或目录
* 🔒 安全修复:移除 confirm 参数,始终需要用户确认,防止绕过
* 🔒 安全修复:添加并发删除检查,防止批量删除攻击
* @param {string} [path] - 文件路径,不传则使用当前路径
* @returns {Promise<boolean>} 用户是否确认及操作是否成功
*/
const deleteFile = async (path) => {
const targetPath = path || filePath.value
if (!targetPath) {
Message.error('请输入文件路径')
return false
}
// 🔒 安全修复:添加调用频率限制(防止批量删除攻击)
if (isDeleting.value) {
Message.warning('正在删除中,请稍候...')
return false
}
const executeDelete = async () => {
isDeleting.value = true
fileLoading.value = true
try {
await deletePathApi(targetPath)
// 清空状态
filePath.value = ''
fileContent.value = ''
fileList.value = []
onSuccess('deleteFile', { path: targetPath })
Message.success('删除成功')
return true
} catch (error) {
onError('deleteFile', error)
Message.error(`删除失败: ${error.message || error}`)
return false
} finally {
fileLoading.value = false
isDeleting.value = false
}
}
// 🔒 安全修复:始终显示确认对话框,无法绕过
return new Promise((resolve) => {
Modal.confirm({
title: '⚠️ 确认删除',
content: `确定要删除 ${targetPath} 吗?此操作不可恢复!`,
okText: '确定删除',
cancelText: '取消',
okButtonProps: { status: 'danger' }, // 红色按钮提醒危险
onOk: async () => {
const result = await executeDelete()
resolve(result)
},
onCancel: () => {
resolve(false)
},
})
})
}
/**
* 选择文件(智能判断是文件还是目录)
* @param {string} path - 文件/目录路径
* @param {Array} fileListData - 文件列表数据
* @returns {Promise<boolean>} 是否成功
*/
const selectFile = async (path, fileListData) => {
if (!path) return false
filePath.value = path
// 从文件列表中查找该项
const item = fileListData.find(f => f.path === path)
if (!item) {
// 如果列表中找不到,尝试根据路径判断
// 简单判断:路径以 / 或 \ 结尾可能是目录
const isDir = path.endsWith('/') || path.endsWith('\\')
if (isDir) {
return await listDirectory(path)
} else {
return await readFile(path)
}
}
if (item.is_dir) {
// 是目录,列出内容
return await listDirectory(path)
} else {
// 是文件,读取内容
return await readFile(path)
}
}
/**
* 清空所有状态
*/
const clearAll = () => {
filePath.value = ''
fileContent.value = ''
fileList.value = []
}
// ========== 持久化 ==========
/**
* 监听路径变化,自动保存到 localStorage
* 用于下次启动时恢复上次访问的路径
*/
watch(filePath, (newPath) => {
try {
if (newPath) {
localStorage.setItem(STORAGE_KEY_LAST_PATH, newPath)
} else {
localStorage.removeItem(STORAGE_KEY_LAST_PATH)
}
} catch (e) {
console.warn('[useFileOperations] 保存路径失败:', e)
}
})
// ========== 返回公共API ==========
return {
// 状态
filePath,
fileContent,
fileList,
fileLoading,
// 方法
listDirectory,
readFile,
writeFile,
deleteFile,
selectFile,
clearAll,
}
}
/**
* @typedef {Object} UseFileOperationsReturn
* @property {Ref<string>} filePath - 当前文件路径
* @property {Ref<string>} fileContent - 文件内容
* @property {Ref<Array>} fileList - 文件列表
* @property {Ref<boolean>} fileLoading - 加载状态
* @property {Function} listDirectory - 列出目录
* @property {Function} readFile - 读取文件
* @property {Function} writeFile - 写入文件
* @property {Function} deleteFile - 删除文件
* @property {Function} selectFile - 智能选择文件
* @property {Function} clearAll - 清空所有状态
*/

View File

@@ -0,0 +1,258 @@
/**
* localStorage 响应式封装
*
* @module composables/useLocalStorage
* @description 提供响应式的 localStorage 数据持久化能力,自动同步数据变化
*/
import { ref, watch, onMounted } from 'vue'
/**
* 创建响应式的 localStorage 绑定
* @param {string} key - localStorage 键名
* @param {*} defaultValue - 默认值
* @param {Object} [options] - 配置选项
* @param {boolean} [options.deep=true] - 是否深度监听对象变化
* @param {boolean} [options.immediate=true] - 是否立即加载
* @returns {UseLocalStorageReturn} 响应式数据和操作方法
*
* @example
* // 基础用法
* const { storedValue, load, save, clear } = useLocalStorage('app-user-name', 'Guest')
*
* // 对象用法
* const { storedValue } = useLocalStorage('app-settings', { theme: 'light' })
*
* @see {@link https://vueuse.org/core/useLocalStorage/} 参考 VueUse 的实现
*/
export function useLocalStorage(key, defaultValue, options = {}) {
const {
deep = true, // 深度监听
immediate = true, // 立即加载
serializer = JSON, // 序列化器
onError = (error) => console.error('localStorage操作失败:', error),
} = options
// 响应式数据
const storedValue = ref(defaultValue)
/**
* 从 localStorage 加载数据
* @returns {boolean} 是否加载成功
*/
const load = () => {
try {
const item = localStorage.getItem(key)
if (item === null || item === undefined) {
storedValue.value = defaultValue
return false
}
// 解析数据
const parsed = serializer.parse(item)
storedValue.value = parsed
return true
} catch (error) {
onError(error)
storedValue.value = defaultValue
return false
}
}
/**
* 保存数据到 localStorage
* @param {*} value - 要保存的值
* @returns {boolean} 是否保存成功
*/
const save = (value) => {
try {
const serialized = serializer.stringify(value)
localStorage.setItem(key, serialized)
return true
} catch (error) {
onError(error)
return false
}
}
/**
* 清除 localStorage 中的数据
* @returns {boolean} 是否清除成功
*/
const clear = () => {
try {
localStorage.removeItem(key)
storedValue.value = defaultValue
return true
} catch (error) {
onError(error)
return false
}
}
// 监听数据变化自动保存
watch(
storedValue,
(newValue) => {
save(newValue)
},
{ deep }
)
// 组件挂载时加载数据
if (immediate) {
onMounted(() => {
load()
})
}
return {
/**
* 响应式数据值
* @type {Ref<any>}
*/
storedValue,
/**
* 手动加载数据
* @type {() => boolean}
*/
load,
/**
* 手动保存数据
* @type {(value: any) => boolean}
*/
save,
/**
* 清除数据
* @type {() => boolean}
*/
clear,
}
}
/**
* @typedef {Object} UseLocalStorageReturn
* @property {Ref<any>} storedValue - 响应式数据
* @property {() => boolean} load - 手动加载函数
* @property {(value: any) => boolean} save - 手动保存函数
* @property {() => boolean} clear - 清除数据函数
*/
/**
* 批量管理多个 localStorage 键
* @param {Object} config - 配置对象键为localStorage键名值为默认值
* @returns {Object} 响应式数据对象
*
* @example
* const {
* theme: themeRef,
* language: languageRef,
* settings: settingsRef
* } = useMultiLocalStorage({
* 'app-theme': 'light',
* 'app-language': 'zh-CN',
* 'app-settings': { fontSize: 14 }
* })
*/
export function useMultiLocalStorage(config) {
const result = {}
Object.keys(config).forEach(key => {
const { storedValue } = useLocalStorage(key, config[key])
result[key] = storedValue
})
return result
}
/**
* localStorage 辅助工具函数
*/
export const localStorageHelpers = {
/**
* 检查 localStorage 是否可用
* @returns {boolean}
*/
isAvailable() {
try {
const test = '__localStorage_test__'
localStorage.setItem(test, test)
localStorage.removeItem(test)
return true
} catch {
return false
}
},
/**
* 获取 localStorage 使用大小(近似值)
* @returns {number} 大小(字节)
*/
getSize() {
let total = 0
for (let key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
total += localStorage[key].length + key.length
}
}
return total
},
/**
* 清空所有 localStorage 数据(谨慎使用)
* @param {string[]} [excludeKeys] - 要排除的键名列表
*/
clearAll(excludeKeys = []) {
const keysToRemove = []
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
if (!excludeKeys.includes(key)) {
keysToRemove.push(key)
}
}
keysToRemove.forEach(key => localStorage.removeItem(key))
},
/**
* 导出所有 localStorage 数据为 JSON
* @returns {string} JSON 字符串
*/
exportToJSON() {
const data = {}
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
data[key] = localStorage.getItem(key)
}
return JSON.stringify(data, null, 2)
},
/**
* 从 JSON 导入 localStorage 数据
* @param {string} jsonString - JSON 字符串
* @param {boolean} [merge=false] - 是否合并false则清空后导入
* @returns {number} 导入的键数量
*/
importFromJSON(jsonString, merge = false) {
try {
const data = JSON.parse(jsonString)
if (!merge) {
localStorageHelpers.clearAll()
}
let count = 0
Object.keys(data).forEach(key => {
localStorage.setItem(key, data[key])
count++
})
return count
} catch (error) {
console.error('导入失败:', error)
return 0
}
},
}