重构:文件系统模块化架构,优化应用启动流程
This commit is contained in:
272
web/src/composables/useFavoriteFiles.js
Normal file
272
web/src/composables/useFavoriteFiles.js
Normal 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
|
||||
}
|
||||
372
web/src/composables/useFileOperations.js
Normal file
372
web/src/composables/useFileOperations.js
Normal 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 - 清空所有状态
|
||||
*/
|
||||
258
web/src/composables/useLocalStorage.js
Normal file
258
web/src/composables/useLocalStorage.js
Normal 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
|
||||
}
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user