feat: add Docker support and update backend dependencies
- Introduced `requirements.docker.txt` for Docker-specific dependencies. - Updated `requirements.txt` to include `psycopg[binary]` and `python-multipart`. - Enhanced test suite in `test_main_endpoints.py` to cover document CRUD operations. - Modified `docker-compose.yml` to include PostgreSQL and frontend services. - Added Nginx configuration for reverse proxying API requests. - Refactored file handling in Vue components to support new document storage backend. - Created new utility functions in `docsApi.js` for document management. - Updated configuration to support new API endpoints for document operations. - Adjusted Vite configuration to proxy API requests to the local backend.
This commit is contained in:
+176
-349
@@ -1,66 +1,18 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import {
|
||||
createDocFolder,
|
||||
createDocTextFile,
|
||||
deleteDocNode,
|
||||
fetchDocBlob,
|
||||
fetchDocNodes,
|
||||
replaceDocBlob,
|
||||
updateDocNode,
|
||||
uploadDocFile,
|
||||
} from '../utils/docsApi'
|
||||
|
||||
const DB_NAME = 'llm-in-text-docs'
|
||||
const DB_VERSION = 1
|
||||
const STORE_NAME = 'nodes'
|
||||
const MAX_FILE_SIZE = 1024 * 1024 * 1024
|
||||
const MAX_TEXT_SIZE = 8 * 1024 * 1024
|
||||
const PREVIEW_TEXT_SIZE = 2 * 1024 * 1024
|
||||
const MAX_NODES = 5000
|
||||
|
||||
let dbPromise = null
|
||||
|
||||
function generateId() {
|
||||
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`
|
||||
}
|
||||
|
||||
function openDatabase() {
|
||||
if (dbPromise) return dbPromise
|
||||
dbPromise = new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION)
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
db.createObjectStore(STORE_NAME, { keyPath: 'id' })
|
||||
}
|
||||
}
|
||||
request.onsuccess = () => resolve(request.result)
|
||||
request.onerror = () => reject(request.error || new Error('打开本地数据库失败'))
|
||||
})
|
||||
return dbPromise
|
||||
}
|
||||
|
||||
async function withStore(mode, handler) {
|
||||
const db = await openDatabase()
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(STORE_NAME, mode)
|
||||
const store = transaction.objectStore(STORE_NAME)
|
||||
let request
|
||||
try {
|
||||
request = handler(store)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
if (request && typeof request.onsuccess === 'function') {
|
||||
request.onsuccess = () => resolve(request.result)
|
||||
request.onerror = () => reject(request.error || new Error('本地数据库操作失败'))
|
||||
} else {
|
||||
transaction.oncomplete = () => resolve(request)
|
||||
transaction.onerror = () => reject(transaction.error || new Error('本地数据库操作失败'))
|
||||
}
|
||||
transaction.onabort = () => reject(transaction.error || new Error('本地数据库操作已取消'))
|
||||
})
|
||||
}
|
||||
|
||||
function cloneRecord(record) {
|
||||
if (!record) return record
|
||||
return {
|
||||
...record,
|
||||
children: undefined
|
||||
}
|
||||
}
|
||||
|
||||
function getExtension(name = '') {
|
||||
const parts = String(name).split('.')
|
||||
return parts.length > 1 ? parts.pop().toLowerCase() : ''
|
||||
@@ -147,7 +99,6 @@ function inferMimeType(name, fallback = '') {
|
||||
|
||||
function isTextFile(record) {
|
||||
const ext = getExtension(record?.name)
|
||||
// 二进制文件不提供预览
|
||||
if (isBinaryExtension(ext)) return false
|
||||
const mime = String(record?.mimeType || '')
|
||||
return isTextExtension(ext) || mime.startsWith('text/') || mime.includes('json') || mime.includes('xml')
|
||||
@@ -169,11 +120,8 @@ function buildTree(records) {
|
||||
continue
|
||||
}
|
||||
const parent = map.get(record.parentId)
|
||||
if (parent?.type === 'folder') {
|
||||
parent.children.push(current)
|
||||
} else {
|
||||
roots.push(current)
|
||||
}
|
||||
if (parent?.type === 'folder') parent.children.push(current)
|
||||
else roots.push(current)
|
||||
}
|
||||
const sorter = (a, b) => {
|
||||
if (a.type !== b.type) return a.type === 'folder' ? -1 : 1
|
||||
@@ -182,9 +130,7 @@ function buildTree(records) {
|
||||
const sortChildren = (nodes) => {
|
||||
nodes.sort(sorter)
|
||||
for (const node of nodes) {
|
||||
if (node.type === 'folder' && Array.isArray(node.children)) {
|
||||
sortChildren(node.children)
|
||||
}
|
||||
if (node.type === 'folder' && Array.isArray(node.children)) sortChildren(node.children)
|
||||
}
|
||||
}
|
||||
sortChildren(roots)
|
||||
@@ -220,94 +166,15 @@ function estimateRecordSize(record) {
|
||||
return 0
|
||||
}
|
||||
|
||||
async function readFilePayload(file) {
|
||||
const mimeType = inferMimeType(file.name, file.type)
|
||||
const ext = getExtension(file.name)
|
||||
const textFile = isTextExtension(ext) || mimeType.startsWith('text/') || mimeType.includes('json') || mimeType.includes('xml')
|
||||
|
||||
if (!textFile) {
|
||||
return {
|
||||
mimeType,
|
||||
size: file.size,
|
||||
storageKind: 'blob',
|
||||
blob: file
|
||||
}
|
||||
}
|
||||
|
||||
// 二进制扩展名文件不尝试读取内容,避免长时间等待
|
||||
if (isBinaryExtension(ext)) {
|
||||
return {
|
||||
mimeType,
|
||||
size: file.size,
|
||||
storageKind: 'blob',
|
||||
blob: file
|
||||
}
|
||||
}
|
||||
|
||||
if (file.size <= MAX_TEXT_SIZE) {
|
||||
const content = await file.text()
|
||||
return {
|
||||
mimeType,
|
||||
size: file.size,
|
||||
storageKind: 'text',
|
||||
content,
|
||||
previewText: content,
|
||||
isTruncatedPreview: false
|
||||
}
|
||||
}
|
||||
const previewText = await file.slice(0, PREVIEW_TEXT_SIZE).text()
|
||||
return {
|
||||
mimeType,
|
||||
size: file.size,
|
||||
storageKind: 'blob',
|
||||
blob: file,
|
||||
previewText,
|
||||
isTruncatedPreview: true
|
||||
}
|
||||
function cloneNode(node) {
|
||||
return JSON.parse(JSON.stringify(node))
|
||||
}
|
||||
|
||||
function createWelcomeRecords() {
|
||||
const folderId = generateId()
|
||||
const fileId = generateId()
|
||||
const now = Date.now()
|
||||
return [
|
||||
{
|
||||
id: folderId,
|
||||
name: '示例文件夹',
|
||||
type: 'folder',
|
||||
parentId: null,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
},
|
||||
{
|
||||
id: fileId,
|
||||
name: '欢迎使用.md',
|
||||
type: 'file',
|
||||
parentId: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
mimeType: 'text/markdown',
|
||||
storageKind: 'text',
|
||||
size: 258,
|
||||
content: [
|
||||
'# 欢迎使用文档模式',
|
||||
'',
|
||||
'这里已经切换为更接近 GitHub 的文件浏览体验。',
|
||||
'',
|
||||
'## 现在支持',
|
||||
'',
|
||||
'- 左侧文件树与快速上传',
|
||||
'- 浏览器本地持久化存储',
|
||||
'- 文本、Markdown、图片、PDF 预览',
|
||||
'- 大文件保留原始文件并显示截断预览'
|
||||
].join('\n'),
|
||||
previewText: '',
|
||||
isTruncatedPreview: false
|
||||
}
|
||||
]
|
||||
}
|
||||
let singleton = null
|
||||
|
||||
export function useFileSystem() {
|
||||
if (singleton) return singleton
|
||||
|
||||
const records = ref([])
|
||||
const selectedId = ref(null)
|
||||
const expandedIds = ref(new Set())
|
||||
@@ -315,6 +182,7 @@ export function useFileSystem() {
|
||||
const contextMenu = ref(null)
|
||||
const error = ref(null)
|
||||
const loading = ref(false)
|
||||
const blobCache = new Map()
|
||||
|
||||
const tree = computed(() => buildTree(records.value))
|
||||
const stats = computed(() => {
|
||||
@@ -329,161 +197,123 @@ export function useFileSystem() {
|
||||
return { fileCount, folderCount, usedBytes }
|
||||
})
|
||||
|
||||
function clearBlobCache(id = null) {
|
||||
if (id) {
|
||||
blobCache.delete(id)
|
||||
return
|
||||
}
|
||||
blobCache.clear()
|
||||
}
|
||||
|
||||
function upsertRecord(nextRecord) {
|
||||
const index = records.value.findIndex((item) => item.id === nextRecord.id)
|
||||
if (index === -1) records.value = [...records.value, nextRecord]
|
||||
else {
|
||||
const next = [...records.value]
|
||||
next[index] = nextRecord
|
||||
records.value = next
|
||||
}
|
||||
return nextRecord
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const nextRecords = await withStore('readonly', (store) => store.getAll())
|
||||
if (!Array.isArray(nextRecords) || nextRecords.length === 0) {
|
||||
const seed = createWelcomeRecords()
|
||||
await Promise.all(seed.map((record) => persistRecord(record)))
|
||||
records.value = seed
|
||||
} else {
|
||||
records.value = nextRecords
|
||||
}
|
||||
records.value = await fetchDocNodes()
|
||||
error.value = null
|
||||
} catch {
|
||||
error.value = '读取本地文件失败,请刷新页面后重试'
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error && err.message ? err.message : '读取文档空间失败,请稍后重试'
|
||||
records.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function persistRecord(record) {
|
||||
return withStore('readwrite', (store) => store.put(cloneRecord(record)))
|
||||
}
|
||||
|
||||
async function deleteRecord(id) {
|
||||
return withStore('readwrite', (store) => store.delete(id))
|
||||
}
|
||||
|
||||
function touchParent(parentId) {
|
||||
if (!parentId) return
|
||||
const parent = records.value.find((item) => item.id === parentId)
|
||||
if (!parent) return
|
||||
parent.updatedAt = Date.now()
|
||||
persistRecord(parent).catch(() => {
|
||||
error.value = '更新目录时间失败'
|
||||
})
|
||||
}
|
||||
|
||||
function createFile(parentId, name, content = '', options = {}) {
|
||||
async function createFile(parentId, name, content = '') {
|
||||
if (records.value.length >= MAX_NODES) {
|
||||
error.value = `文件数量不能超过 ${MAX_NODES} 个`
|
||||
return false
|
||||
}
|
||||
const size = typeof options.size === 'number' ? options.size : new Blob([content]).size
|
||||
if (size > MAX_FILE_SIZE) {
|
||||
error.value = '单个文件不能超过 1GB'
|
||||
try {
|
||||
const node = await createDocTextFile(name, parentId || null, content)
|
||||
upsertRecord(node)
|
||||
if (parentId) {
|
||||
const next = new Set(expandedIds.value)
|
||||
next.add(parentId)
|
||||
expandedIds.value = next
|
||||
}
|
||||
selectedId.value = node.id
|
||||
error.value = null
|
||||
return true
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error && err.message ? err.message : '创建文件失败'
|
||||
return false
|
||||
}
|
||||
const now = Date.now()
|
||||
const file = {
|
||||
id: generateId(),
|
||||
name,
|
||||
type: 'file',
|
||||
parentId: parentId || null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
mimeType: inferMimeType(name, options.mimeType),
|
||||
storageKind: options.storageKind || 'text',
|
||||
size,
|
||||
content: options.content ?? content,
|
||||
previewText: options.previewText ?? '',
|
||||
isTruncatedPreview: Boolean(options.isTruncatedPreview),
|
||||
blob: options.blob || null
|
||||
}
|
||||
records.value = [...records.value, file]
|
||||
if (parentId) {
|
||||
const next = new Set(expandedIds.value)
|
||||
next.add(parentId)
|
||||
expandedIds.value = next
|
||||
}
|
||||
selectedId.value = file.id
|
||||
error.value = null
|
||||
persistRecord(file).catch(() => {
|
||||
error.value = '保存文件失败,可能是浏览器存储空间不足'
|
||||
})
|
||||
touchParent(parentId)
|
||||
return true
|
||||
}
|
||||
|
||||
function updateFile(id, nextValue, options = {}) {
|
||||
async function updateFile(id, nextValue, options = {}) {
|
||||
const file = records.value.find((item) => item.id === id && item.type === 'file')
|
||||
if (!file) return false
|
||||
|
||||
const nextName = options.name || file.name
|
||||
const isBlobValue = nextValue instanceof Blob
|
||||
const nextContent = isBlobValue ? (options.content ?? '') : String(options.content ?? nextValue ?? '')
|
||||
const nextSize = typeof options.size === 'number'
|
||||
? options.size
|
||||
: isBlobValue
|
||||
? nextValue.size
|
||||
: new Blob([nextContent]).size
|
||||
|
||||
if (nextSize > MAX_FILE_SIZE) {
|
||||
error.value = '单个文件不能超过 1GB'
|
||||
try {
|
||||
let node
|
||||
if (nextValue instanceof Blob) {
|
||||
const filename = options.name || file.name
|
||||
const upload = nextValue instanceof File
|
||||
? nextValue
|
||||
: new File([nextValue], filename, { type: options.mimeType || nextValue.type || file.mimeType || '' })
|
||||
node = await replaceDocBlob(id, upload)
|
||||
} else {
|
||||
const content = String(options.content ?? nextValue ?? '')
|
||||
node = await updateDocNode(id, {
|
||||
name: options.name || file.name,
|
||||
content,
|
||||
})
|
||||
}
|
||||
upsertRecord(node)
|
||||
clearBlobCache(id)
|
||||
error.value = null
|
||||
return true
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error && err.message ? err.message : '保存文件失败'
|
||||
return false
|
||||
}
|
||||
|
||||
file.name = nextName
|
||||
file.updatedAt = Date.now()
|
||||
file.mimeType = inferMimeType(nextName, options.mimeType || (isBlobValue ? nextValue.type : file.mimeType))
|
||||
file.size = nextSize
|
||||
file.storageKind = options.storageKind || (isBlobValue ? 'blob' : 'text')
|
||||
file.content = nextContent
|
||||
file.previewText = options.previewText ?? (file.storageKind === 'text' ? nextContent : '')
|
||||
file.isTruncatedPreview = Boolean(options.isTruncatedPreview)
|
||||
file.blob = isBlobValue ? nextValue : null
|
||||
records.value = [...records.value]
|
||||
error.value = null
|
||||
|
||||
persistRecord(file).catch(() => {
|
||||
error.value = '保存文件失败,可能是浏览器存储空间不足'
|
||||
})
|
||||
touchParent(file.parentId)
|
||||
return true
|
||||
}
|
||||
|
||||
function createFolder(parentId, name) {
|
||||
async function createFolder(parentId, name) {
|
||||
if (records.value.length >= MAX_NODES) {
|
||||
error.value = `目录项数量不能超过 ${MAX_NODES} 个`
|
||||
return false
|
||||
}
|
||||
const now = Date.now()
|
||||
const folder = {
|
||||
id: generateId(),
|
||||
name,
|
||||
type: 'folder',
|
||||
parentId: parentId || null,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
try {
|
||||
const node = await createDocFolder(name, parentId || null)
|
||||
upsertRecord(node)
|
||||
if (parentId) {
|
||||
const next = new Set(expandedIds.value)
|
||||
next.add(parentId)
|
||||
expandedIds.value = next
|
||||
}
|
||||
selectedId.value = node.id
|
||||
error.value = null
|
||||
return true
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error && err.message ? err.message : '创建文件夹失败'
|
||||
return false
|
||||
}
|
||||
records.value = [...records.value, folder]
|
||||
if (parentId) {
|
||||
const next = new Set(expandedIds.value)
|
||||
next.add(parentId)
|
||||
expandedIds.value = next
|
||||
}
|
||||
selectedId.value = folder.id
|
||||
error.value = null
|
||||
persistRecord(folder).catch(() => {
|
||||
error.value = '保存文件夹失败'
|
||||
})
|
||||
touchParent(parentId)
|
||||
return true
|
||||
}
|
||||
|
||||
function rename(id, newName) {
|
||||
async function rename(id, newName) {
|
||||
const node = records.value.find((item) => item.id === id)
|
||||
if (!node) return false
|
||||
node.name = newName
|
||||
node.updatedAt = Date.now()
|
||||
error.value = null
|
||||
persistRecord(node).catch(() => {
|
||||
error.value = '重命名失败'
|
||||
})
|
||||
return true
|
||||
try {
|
||||
const updated = await updateDocNode(id, { name: newName })
|
||||
upsertRecord(updated)
|
||||
clearBlobCache(id)
|
||||
error.value = null
|
||||
return true
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error && err.message ? err.message : '重命名失败'
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function collectDescendantIds(id) {
|
||||
@@ -501,23 +331,21 @@ export function useFileSystem() {
|
||||
return [...ids]
|
||||
}
|
||||
|
||||
function remove(id) {
|
||||
async function remove(id) {
|
||||
const ids = new Set(collectDescendantIds(id))
|
||||
const deletingSelected = selectedId.value && ids.has(selectedId.value)
|
||||
records.value = records.value.filter((item) => !ids.has(item.id))
|
||||
if (deletingSelected) selectedId.value = null
|
||||
if (clipboard.value?.nodeId && ids.has(clipboard.value.nodeId)) {
|
||||
clipboard.value = null
|
||||
try {
|
||||
await deleteDocNode(id)
|
||||
records.value = records.value.filter((item) => !ids.has(item.id))
|
||||
if (selectedId.value && ids.has(selectedId.value)) selectedId.value = null
|
||||
if (clipboard.value?.nodeId && ids.has(clipboard.value.nodeId)) clipboard.value = null
|
||||
if (clipboard.value?.node?.id && ids.has(clipboard.value.node.id)) clipboard.value = null
|
||||
ids.forEach((currentId) => clearBlobCache(currentId))
|
||||
error.value = null
|
||||
return true
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error && err.message ? err.message : '删除文件失败'
|
||||
return false
|
||||
}
|
||||
if (clipboard.value?.node?.id && ids.has(clipboard.value.node.id)) {
|
||||
clipboard.value = null
|
||||
}
|
||||
error.value = null
|
||||
ids.forEach((currentId) => {
|
||||
deleteRecord(currentId).catch(() => {
|
||||
error.value = '删除文件失败'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function select(id) {
|
||||
@@ -536,19 +364,13 @@ export function useFileSystem() {
|
||||
function copy(id) {
|
||||
const node = findNode(tree.value, id)
|
||||
if (!node) return
|
||||
clipboard.value = {
|
||||
mode: 'copy',
|
||||
node: JSON.parse(JSON.stringify(node))
|
||||
}
|
||||
clipboard.value = { mode: 'copy', node: cloneNode(node) }
|
||||
}
|
||||
|
||||
function cut(id) {
|
||||
const node = records.value.find((item) => item.id === id)
|
||||
if (!node) return
|
||||
clipboard.value = {
|
||||
mode: 'cut',
|
||||
nodeId: node.id
|
||||
}
|
||||
clipboard.value = { mode: 'cut', nodeId: node.id }
|
||||
}
|
||||
|
||||
function isDescendantOf(sourceId, targetParentId) {
|
||||
@@ -560,58 +382,58 @@ export function useFileSystem() {
|
||||
return false
|
||||
}
|
||||
|
||||
function duplicateNode(node, targetParentId) {
|
||||
const now = Date.now()
|
||||
const clonedId = generateId()
|
||||
const record = {
|
||||
...cloneRecord(node),
|
||||
id: clonedId,
|
||||
parentId: targetParentId,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
}
|
||||
records.value = [...records.value, record]
|
||||
persistRecord(record).catch(() => {
|
||||
error.value = '复制文件失败'
|
||||
})
|
||||
async function duplicateNode(node, targetParentId) {
|
||||
let created
|
||||
if (node.type === 'folder') {
|
||||
created = await createDocFolder(node.name, targetParentId)
|
||||
upsertRecord(created)
|
||||
for (const child of node.children || []) {
|
||||
duplicateNode(child, clonedId)
|
||||
await duplicateNode(child, created.id)
|
||||
}
|
||||
return created
|
||||
}
|
||||
|
||||
if (node.storageKind === 'blob') {
|
||||
const blob = await getFileBlob(node)
|
||||
const upload = new File([blob], node.name, { type: node.mimeType || blob.type || inferMimeType(node.name) })
|
||||
created = await uploadDocFile(upload, targetParentId)
|
||||
} else {
|
||||
created = await createDocTextFile(node.name, targetParentId, node.content || node.previewText || '')
|
||||
}
|
||||
upsertRecord(created)
|
||||
return created
|
||||
}
|
||||
|
||||
function paste(targetParentId) {
|
||||
async function paste(targetParentId) {
|
||||
if (!clipboard.value) return
|
||||
if (clipboard.value.mode === 'cut') {
|
||||
const node = records.value.find((item) => item.id === clipboard.value.nodeId)
|
||||
if (!node) {
|
||||
try {
|
||||
if (clipboard.value.mode === 'cut') {
|
||||
const node = records.value.find((item) => item.id === clipboard.value.nodeId)
|
||||
if (!node) {
|
||||
clipboard.value = null
|
||||
return
|
||||
}
|
||||
if (node.id === targetParentId || (targetParentId && isDescendantOf(node.id, targetParentId))) {
|
||||
error.value = '不能移动到自身或子目录中'
|
||||
return
|
||||
}
|
||||
const updated = await updateDocNode(node.id, { parentId: targetParentId || null })
|
||||
upsertRecord(updated)
|
||||
clipboard.value = null
|
||||
error.value = null
|
||||
return
|
||||
}
|
||||
if (node.id === targetParentId || (targetParentId && isDescendantOf(node.id, targetParentId))) {
|
||||
error.value = '不能移动到自身或子目录中'
|
||||
const source = clipboard.value.node
|
||||
if (!source) return
|
||||
if (records.value.length >= MAX_NODES) {
|
||||
error.value = `目录项数量不能超过 ${MAX_NODES} 个`
|
||||
return
|
||||
}
|
||||
node.parentId = targetParentId || null
|
||||
node.updatedAt = Date.now()
|
||||
persistRecord(node).catch(() => {
|
||||
error.value = '移动文件失败'
|
||||
})
|
||||
touchParent(targetParentId)
|
||||
clipboard.value = null
|
||||
await duplicateNode(source, targetParentId || null)
|
||||
error.value = null
|
||||
return
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error && err.message ? err.message : '粘贴失败'
|
||||
}
|
||||
const source = clipboard.value.node
|
||||
if (!source) return
|
||||
if (records.value.length >= MAX_NODES) {
|
||||
error.value = `目录项数量不能超过 ${MAX_NODES} 个`
|
||||
return
|
||||
}
|
||||
duplicateNode(source, targetParentId || null)
|
||||
touchParent(targetParentId)
|
||||
error.value = null
|
||||
}
|
||||
|
||||
function canPaste() {
|
||||
@@ -699,12 +521,14 @@ export function useFileSystem() {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
const payload = await readFilePayload(file)
|
||||
const created = createFile(parentId, file.name, payload.content || '', payload)
|
||||
if (created) success += 1
|
||||
else failed.push({ name: file.name, reason: error.value || '创建文件失败' })
|
||||
} catch {
|
||||
failed.push({ name: file.name, reason: '读取文件失败' })
|
||||
const node = await uploadDocFile(file, parentId)
|
||||
upsertRecord(node)
|
||||
success += 1
|
||||
} catch (err) {
|
||||
failed.push({
|
||||
name: file.name,
|
||||
reason: err instanceof Error && err.message ? err.message : '上传文件失败',
|
||||
})
|
||||
}
|
||||
}
|
||||
if (success > 0 && parentId) {
|
||||
@@ -715,19 +539,20 @@ export function useFileSystem() {
|
||||
return { success, failed }
|
||||
}
|
||||
|
||||
function getFileBlob(node) {
|
||||
async function getFileBlob(node) {
|
||||
if (!node || node.type !== 'file') return null
|
||||
if (node.blob instanceof Blob) return node.blob
|
||||
if (typeof node.content === 'string') {
|
||||
return new Blob([node.content], { type: inferMimeType(node.name, node.mimeType) })
|
||||
if (blobCache.has(node.id)) return blobCache.get(node.id)
|
||||
if (node.storageKind === 'text' && typeof node.content === 'string' && node.content && !node.isTruncatedPreview) {
|
||||
const blob = new Blob([node.content], { type: inferMimeType(node.name, node.mimeType) })
|
||||
blobCache.set(node.id, blob)
|
||||
return blob
|
||||
}
|
||||
if (typeof node.previewText === 'string' && node.previewText) {
|
||||
return new Blob([node.previewText], { type: inferMimeType(node.name, node.mimeType) })
|
||||
}
|
||||
return null
|
||||
const blob = await fetchDocBlob(node.id)
|
||||
blobCache.set(node.id, blob)
|
||||
return blob
|
||||
}
|
||||
|
||||
return {
|
||||
singleton = {
|
||||
tree,
|
||||
selectedId,
|
||||
expandedIds,
|
||||
@@ -761,4 +586,6 @@ export function useFileSystem() {
|
||||
MAX_FILE_SIZE,
|
||||
MAX_NODES
|
||||
}
|
||||
|
||||
return singleton
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user