feat(web): 引入 Vite 前端应用并扩展仓库忽略规则

将整套 web 源码纳入仓库,并为 web/node_modules、构建产物及本地环境文件配置 .gitignore,同时移除占位用的 assets/.gitkeep。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
caopeng
2026-05-17 15:22:29 +08:00
parent 759637dba2
commit a254aae503
161 changed files with 69862 additions and 11 deletions

View File

@@ -0,0 +1,343 @@
// WebDAV Driver for VueFinder
import request from '../../utils/request'
import { getApiBaseUrl } from '../../utils/config'
// 解析 WebDAV PROPFIND XML 响应
export function parseWebDAVXML(xmlString) {
try {
// 确保输入是字符串
if (typeof xmlString !== 'string') {
console.error('parseWebDAVXML: 输入不是字符串类型', typeof xmlString, xmlString)
throw new Error('XML 响应必须是字符串类型')
}
// 清理和提取有效的 XML 内容
let cleanedXml = xmlString.trim()
// 如果响应包含多个 XML 文档或末尾有额外内容,尝试提取第一个完整的 XML 文档
// 查找第一个 <?xml 或 <multistatus 或 <d:multistatus 标签
const xmlStartPattern = /(<\?xml[\s\S]*?<[^:]*:?multistatus)/i
const match = cleanedXml.match(xmlStartPattern)
if (match) {
// 找到 XML 开始位置
const startIndex = cleanedXml.indexOf(match[1])
// 查找对应的结束标签
const endTagPattern = /<\/[^:]*:?multistatus>/i
const endMatch = cleanedXml.substring(startIndex).match(endTagPattern)
if (endMatch) {
// 提取完整的 XML 文档
const endIndex = startIndex + endMatch.index + endMatch[0].length
cleanedXml = cleanedXml.substring(startIndex, endIndex)
// console.log('提取了有效的 XML 片段,原始长度:', xmlString.length, '清理后长度:', cleanedXml.length)
} else {
console.warn('未找到 XML 结束标签,使用原始响应')
}
}
// 如果仍然没有找到有效的 XML尝试查找第一个 <multistatus> 到 </multistatus>
if (!cleanedXml.includes('multistatus') && !cleanedXml.includes('<?xml')) {
console.warn('响应可能不是有效的 XML 格式:', cleanedXml.substring(0, 200))
}
const parser = new DOMParser()
const xmlDoc = parser.parseFromString(cleanedXml, 'text/xml')
// 检查是否有解析错误
const parserError = xmlDoc.querySelector('parsererror')
if (parserError) {
// 记录原始响应的一部分用于调试
console.error('XML 解析错误详情:', {
errorText: parserError.textContent,
xmlPreview: cleanedXml.substring(0, 500),
xmlLength: cleanedXml.length,
originalLength: xmlString.length
})
throw new Error('XML 解析失败: ' + parserError.textContent)
}
// 辅助函数:通过本地名称查找元素(处理命名空间)
const getElementByLocalName = (parent, localName) => {
if (!parent || !parent.getElementsByTagName) return null
const allElements = parent.getElementsByTagName('*')
for (let i = 0; i < allElements.length; i++) {
const elem = allElements[i]
const elemLocalName = elem.localName || elem.tagName?.split(':').pop() || elem.tagName
if (elemLocalName === localName || elem.tagName === localName || elem.tagName?.endsWith(':' + localName)) {
return elem
}
}
return null
}
const getElementTextByLocalName = (parent, localName) => {
const element = getElementByLocalName(parent, localName)
return element?.textContent || ''
}
// 获取所有 response 节点
const allElements = xmlDoc.getElementsByTagName('*')
const responses = []
for (let i = 0; i < allElements.length; i++) {
const elem = allElements[i]
const elemLocalName = elem.localName || elem.tagName?.split(':').pop() || elem.tagName
if (elemLocalName === 'response' || elem.tagName === 'response' || elem.tagName?.endsWith(':response')) {
responses.push(elem)
}
}
const fileList = []
responses.forEach((response) => {
try {
const href = getElementTextByLocalName(response, 'href')
// 跳过根路径
if (href === '/' || !href) {
return
}
const propstat = getElementByLocalName(response, 'propstat')
if (!propstat) return
const prop = getElementByLocalName(propstat, 'prop')
if (!prop) return
// 检查是否为集合(文件夹)
const resourcetype = getElementByLocalName(prop, 'resourcetype')
const collectionNode = resourcetype ? getElementByLocalName(resourcetype, 'collection') : null
const isCollection = collectionNode !== null
// 获取文件大小
const getcontentlengthText = getElementTextByLocalName(prop, 'getcontentlength')
const size = getcontentlengthText ? parseInt(getcontentlengthText, 10) : 0
// 获取修改时间
const lastModified = getElementTextByLocalName(prop, 'getlastmodified')
// 获取内容类型
const getcontenttypeText = getElementTextByLocalName(prop, 'getcontenttype')
const contentType = getcontenttypeText || (isCollection ? 'directory' : 'application/octet-stream')
// 从路径中提取文件名和相对路径
// href 格式可能是: /folda/ 或 /filea可能是 URL 编码的
// 先对 href 进行 URL 解码
let decodedHref = href
try {
decodedHref = decodeURIComponent(href)
} catch (e) {
// 如果解码失败,使用原始 href
decodedHref = href
}
const cleanHref = decodedHref.replace(/\/$/, '') // 移除末尾斜杠
const pathParts = cleanHref.split('/').filter(p => p)
const name = pathParts[pathParts.length - 1] || cleanHref || 'unknown'
// 构建相对路径(相对于 webdav 根目录)
// 例如: /folda/ -> folda, /filea -> filea
// 注意:相对路径也需要解码后的值
const relativePath = pathParts.join('/')
fileList.push({
href: href,
name: name,
isDirectory: isCollection,
size: size,
lastModified: lastModified,
contentType: contentType,
path: relativePath, // 相对路径,例如: "folda" 或 "filea"
})
} catch (err) {
console.warn('解析单个 response 失败:', err)
}
})
return fileList
} catch (error) {
console.error('解析 WebDAV XML 失败:', error)
throw error
}
}
// 将 WebDAV 文件列表转换为 VueFinder 格式
function convertToVueFinderFormat(webdavFiles, basePath = '') {
const items = []
webdavFiles.forEach(file => {
// VueFinder 期望的格式path应该以/开头
let itemPath
if (basePath && basePath !== '' && basePath !== '/') {
const normalizedBasePath = basePath.startsWith('/') ? basePath : '/' + basePath
itemPath = normalizedBasePath + '/' + file.name
} else {
// 根目录下的文件path应该是 /folda 或 /filea
itemPath = '/' + file.name
}
const item = {
name: file.name,
path: itemPath, // 绝对路径,例如: /folda 或 /filea
type: file.isDirectory ? 'dir' : 'file',
size: file.size || 0,
modified: file.lastModified ? new Date(file.lastModified).getTime() : Date.now(),
isDirectory: file.isDirectory,
}
items.push(item)
})
// 目录排在前面
items.sort((a, b) => {
if (a.type === 'dir' && b.type !== 'dir') return -1
if (a.type !== 'dir' && b.type === 'dir') return 1
return a.name.localeCompare(b.name)
})
// 返回的path表示当前目录路径
// 对于根目录path应该是空字符串对于子目录path应该是相对路径不带开头的/
let normalizedPath = basePath || ''
if (normalizedPath === '/') {
normalizedPath = ''
}
return {
items: items,
path: normalizedPath, // 当前目录路径,根目录为空字符串
}
}
// 创建 WebDAV Driver
export function createWebDAVDriver() {
const baseURL = getApiBaseUrl() || ''
const basePath = `${baseURL}/webdav`
return {
async list(params = {}) {
try {
// 构建路径,确保 URL 格式为 webdav/ 或 webdav/path/
let path = params.path || ''
// 移除开头的 /因为我们要构建相对路径用于API请求
if (path.startsWith('/')) {
path = path.substring(1)
}
// 如果 path 为空,请求 webdav/,否则请求 webdav/path/
const url = path ? `/webdav/${path}${path.endsWith('/') ? '' : '/'}` : '/webdav/'
// console.log('WebDAV list 请求:', { url, path, params })
// 发送 PROPFIND 请求,使用 Depth: 1 请求头
const response = await request({
url: url,
method: 'PROPFIND',
headers: {
'Depth': '1'
}
})
// console.log('WebDAV 原始响应:', response)
// 解析 XML 响应
const fileList = parseWebDAVXML(response)
// console.log('解析后的文件列表:', fileList)
// 转换为 VueFinder 格式
// normalizedPath用于构建item的path应该保持原样不带开头的/
const normalizedPath = path || ''
const vueFinderData = convertToVueFinderFormat(fileList, normalizedPath)
// console.log('VueFinder 格式数据:', vueFinderData)
// console.log('VueFinder items 数量:', vueFinderData.items.length)
// console.log('VueFinder items 详情:', vueFinderData.items)
// 确保返回的数据格式正确
if (!vueFinderData.items || !Array.isArray(vueFinderData.items)) {
console.error('VueFinder 数据格式错误:', vueFinderData)
return { items: [], path: normalizedPath }
}
// 验证每个 item 的格式,确保所有必需字段都存在
vueFinderData.items.forEach((item, index) => {
if (!item.name || !item.path || !item.type) {
console.warn(`Item ${index} 格式不完整:`, item)
}
// 确保path是字符串格式
if (typeof item.path !== 'string') {
item.path = String(item.path)
}
// 确保size是数字
if (typeof item.size !== 'number') {
item.size = Number(item.size) || 0
}
})
console.log('返回给 VueFinder 的数据:', JSON.stringify(vueFinderData, null, 2))
console.log('最终返回数据:', vueFinderData)
return vueFinderData
} catch (error) {
console.error('WebDAV list 失败:', error)
throw error
}
},
async delete(params) {
return request({
url: `/webdav/${params.path}`,
method: 'DELETE'
})
},
async rename(params) {
// WebDAV MOVE 方法用于重命名
// 对新路径进行 URL 编码(对每个路径段分别编码,保留斜杠)
const encodedNewPath = params.newPath
.split('/')
.map(segment => encodeURIComponent(segment))
.join('/')
return request({
url: `/webdav/${params.path}`,
method: 'MOVE',
headers: {
'Destination': `/webdav/${encodedNewPath}`
}
})
},
getDownloadUrl(params) {
const token = localStorage.getItem('token')
const uid = localStorage.getItem('uid')
const path = params.path?.startsWith('/') ? params.path.substring(1) : params.path
return `${basePath}/${path}?token=${token}&uid=${uid}`
},
getPreviewUrl(params) {
return this.getDownloadUrl(params)
},
async search(params) {
// 搜索功能可能需要遍历所有目录
// 这里先实现简单的列表
const result = await this.list({ path: params.path || '' })
return result.items.filter(item =>
item.name.toLowerCase().includes(params.query.toLowerCase())
)
},
async save(params) {
// 上传文件使用 PUT 方法
return request({
url: `/webdav/${params.path}`,
method: 'PUT',
data: params.content,
headers: {
'Content-Type': params.contentType || 'application/octet-stream'
}
})
}
}
}