Files
llm-in-text/src/composables/useFileSystem.js
T

363 lines
8.4 KiB
JavaScript
Raw Normal View History

import { ref, computed, watch } from 'vue'
const STORAGE_KEY = 'llm-in-text-file-system'
const MAX_FILE_SIZE = 50 * 1024 * 1024 // 50MB
const MAX_FILES = 100
const MAX_FOLDERS = 50
function generateId() {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 9)
}
function countFilesAndFolders(nodes) {
let files = 0
let folders = 0
function traverse(items) {
for (const item of items) {
if (item.type === 'folder') {
folders++
traverse(item.children || [])
} else {
files++
}
}
}
traverse(nodes)
return { files, folders }
}
function findNode(nodes, id) {
for (const node of nodes) {
if (node.id === id) return node
if (node.type === 'folder') {
const found = findNode(node.children || [], id)
if (found) return found
}
}
return null
}
function findParent(nodes, id, parent = null) {
for (const node of nodes) {
if (node.id === id) return parent
if (node.type === 'folder') {
const found = findParent(node.children || [], id, node)
if (found !== undefined) return found
}
}
return undefined
}
function removeNode(nodes, id) {
return nodes.filter(n => n.id !== id).map(n => {
if (n.type === 'folder') {
return { ...n, children: removeNode(n.children || [], id) }
}
return n
})
}
function getPath(nodes, id, path = []) {
for (const node of nodes) {
if (node.id === id) return [...path, node]
if (node.type === 'folder') {
const found = getPath(node.children || [], id, [...path, node])
if (found) return found
}
}
return null
}
export function useFileSystem() {
const tree = ref([])
const selectedId = ref(null)
const expandedIds = ref(new Set())
const clipboard = ref(null) // { mode: 'copy' | 'cut', node: {...} }
const contextMenu = ref(null) // { x, y, node }
const error = ref(null)
function load() {
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
tree.value = JSON.parse(stored)
}
} catch {
tree.value = []
}
}
function save() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tree.value))
} catch {
error.value = '存储空间不足'
}
}
watch(tree, save, { deep: true })
function createFile(parentId, name, content = '') {
const { files } = countFilesAndFolders(tree.value)
if (files >= MAX_FILES) {
error.value = `文件数量已达上限(${MAX_FILES}个)`
return false
}
if (new Blob([content]).size > MAX_FILE_SIZE) {
error.value = '文件大小不能超过 50MB'
return false
}
const newFile = {
id: generateId(),
name,
type: 'file',
content,
parentId,
createdAt: Date.now(),
updatedAt: Date.now()
}
if (!parentId) {
tree.value.push(newFile)
} else {
const parent = findNode(tree.value, parentId)
if (parent && parent.type === 'folder') {
parent.children = parent.children || []
parent.children.push(newFile)
}
}
error.value = null
return true
}
function createFolder(parentId, name) {
const { folders } = countFilesAndFolders(tree.value)
if (folders >= MAX_FOLDERS) {
error.value = `文件夹数量已达上限(${MAX_FOLDERS}个)`
return false
}
const newFolder = {
id: generateId(),
name,
type: 'folder',
children: [],
parentId,
createdAt: Date.now(),
updatedAt: Date.now()
}
if (!parentId) {
tree.value.push(newFolder)
} else {
const parent = findNode(tree.value, parentId)
if (parent && parent.type === 'folder') {
parent.children = parent.children || []
parent.children.push(newFolder)
}
}
error.value = null
return true
}
function rename(id, newName) {
const node = findNode(tree.value, id)
if (node) {
node.name = newName
node.updatedAt = Date.now()
error.value = null
return true
}
return false
}
function remove(id) {
tree.value = removeNode(tree.value, id)
if (selectedId.value === id) selectedId.value = null
error.value = null
}
function select(id) {
selectedId.value = id
const node = findNode(tree.value, id)
if (node && node.type === 'folder') {
toggleFolder(id)
}
}
function toggleFolder(id) {
const node = findNode(tree.value, id)
if (!node || node.type !== 'folder') return
const set = new Set(expandedIds.value)
if (set.has(id)) {
set.delete(id)
} else {
set.add(id)
}
expandedIds.value = set
}
function copy(id) {
const node = findNode(tree.value, id)
if (node) {
clipboard.value = { mode: 'copy', node: JSON.parse(JSON.stringify(node)) }
}
}
function cut(id) {
const node = findNode(tree.value, id)
if (node) {
clipboard.value = { mode: 'cut', node: JSON.parse(JSON.stringify(node)) }
}
}
function paste(targetParentId) {
if (!clipboard.value) return
const { mode, node } = clipboard.value
if (mode === 'cut') {
const oldParent = findParent(tree.value, node.id)
if (oldParent) {
oldParent.children = (oldParent.children || []).filter(c => c.id !== node.id)
} else {
tree.value = tree.value.filter(n => n.id !== node.id)
}
node.parentId = targetParentId || null
if (targetParentId) {
const target = findNode(tree.value, targetParentId)
if (target && target.type === 'folder') {
target.children = target.children || []
target.children.push(node)
}
} else {
tree.value.push(node)
}
clipboard.value = null
} else {
const { files, folders } = countFilesAndFolders(tree.value)
function countInNode(n) {
let f = 0, fl = 0
if (n.type === 'folder') {
fl = 1
for (const c of (n.children || [])) {
const sub = countInNode(c)
f += sub.f
fl += sub.fl
}
} else {
f = 1
}
return { f, fl }
}
const counts = countInNode(node)
if (files + counts.f > MAX_FILES) {
error.value = `文件数量将达上限`
return
}
if (folders + counts.fl > MAX_FOLDERS) {
error.value = `文件夹数量将达上限`
return
}
function cloneWithNewIds(n) {
const clone = { ...n, id: generateId(), createdAt: Date.now(), updatedAt: Date.now() }
if (clone.type === 'folder') {
clone.children = (n.children || []).map(c => cloneWithNewIds(c))
}
return clone
}
const cloned = cloneWithNewIds(node)
cloned.parentId = targetParentId || null
if (targetParentId) {
const target = findNode(tree.value, targetParentId)
if (target && target.type === 'folder') {
target.children = target.children || []
target.children.push(cloned)
}
} else {
tree.value.push(cloned)
}
}
error.value = null
}
function canPaste() {
return clipboard.value !== null
}
function clearClipboard() {
clipboard.value = null
}
function getSelectedNode() {
return selectedId.value ? findNode(tree.value, selectedId.value) : null
}
function getBreadcrumbPath(id) {
return getPath(tree.value, id) || []
}
function showContextMenu(x, y, node) {
contextMenu.value = { x, y, node }
}
function hideContextMenu() {
contextMenu.value = null
}
function getExtension(name) {
const parts = name.split('.')
return parts.length > 1 ? parts.pop().toLowerCase() : ''
}
function getFileIcon(name) {
const ext = getExtension(name)
const iconMap = {
md: 'markdown',
txt: 'text',
json: 'json',
js: 'javascript',
ts: 'typescript',
css: 'css',
html: 'html',
py: 'python',
vue: 'vue',
xml: 'xml',
yaml: 'yaml',
yml: 'yaml',
csv: 'csv',
log: 'log',
sql: 'sql'
}
return iconMap[ext] || 'file'
}
return {
tree,
selectedId,
expandedIds,
clipboard,
contextMenu,
error,
load,
createFile,
createFolder,
rename,
remove,
select,
toggleFolder,
copy,
cut,
paste,
canPaste,
clearClipboard,
getSelectedNode,
getBreadcrumbPath,
showContextMenu,
hideContextMenu,
getFileIcon,
getExtension,
MAX_FILE_SIZE,
MAX_FILES,
MAX_FOLDERS
}
}