/** * 文件工具函数集合 * * @module utils/fileUtils * @description 提供文件相关的通用工具函数,避免代码重复 */ import { FILE_ICON_MAP, FILE_ICONS, BYTE_UNITS, FILE_SIZE_FORMAT } from './constants' /** * 路径分隔符正则(匹配 Windows 和 Unix 风格) * @type {RegExp} */ const PATH_SEPARATOR_REGEX = /[/\\]/ /** * 规范化路径分隔符(统一为正斜杠) * @param {string} path - 文件路径 * @returns {string} 规范化后的路径 */ export const normalizePathSeparators = (path) => { if (!path) return '' return path.replace(/\\/g, '/') } /** * HTML 转义,防止 XSS * @param {string} str - 原始字符串 * @returns {string} 转义后的字符串 */ export const escapeHtml = (str) => { if (str == null) return '' return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') } /** * 轻量 HTML 消毒(用于渲染远程 Markdown 等不可信 HTML 片段) * 移除 script/iframe/object/embed 标签和 on* 事件属性 */ export const sanitizeHtml = (html) => { if (!html) return '' return String(html) .replace(/|$)/gi, '') .replace(/|$)/gi, '') .replace(/|$)/gi, '') .replace(/]*\/?>/gi, '') .replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '') .replace(/\s+on\w+\s*=\s*[^\s>]*/gi, '') } /** * 获取文件扩展名(路径安全) * @param {string} path - 文件路径 * @returns {string} 扩展名(小写,不含点号) */ export const getExt = (path) => { if (!path) return '' const dot = path.lastIndexOf('.') const slash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) if (dot === -1 || dot < slash) return '' return path.slice(dot + 1).toLowerCase() } /** * 格式化文件大小 * @param {number} bytes - 文件大小(字节) * @returns {string} 格式化后的文件大小字符串 * * @example * formatBytes(1024) // "1.00 KB" * formatBytes(1048576) // "1.00 MB" * formatBytes(0) // "0 B" */ export function formatBytes(bytes) { if (!bytes || bytes === 0) return '0 B' if (typeof bytes !== 'number' || isNaN(bytes)) return '0 B' const unit = FILE_SIZE_FORMAT.UNIT const decimals = FILE_SIZE_FORMAT.DECIMAL_PLACES if (bytes < unit) return bytes + ' B' const exp = Math.min(Math.floor(Math.log(bytes) / Math.log(unit)), BYTE_UNITS.length - 1) const value = bytes / Math.pow(unit, exp) const unitSymbol = BYTE_UNITS[exp] return value.toFixed(decimals) + ' ' + unitSymbol } /** * 从文件路径中提取文件名 * @param {string} path - 文件路径 * @returns {string} 文件名 * * @example * getFileName('/home/user/docs/file.txt') // "file.txt" * getFileName('C:\\Users\\user\\file.txt') // "file.txt" * getFileName('file.txt') // "file.txt" */ export function getFileName(path) { if (!path) return '' const parts = path.split(PATH_SEPARATOR_REGEX) return parts[parts.length - 1] || path } /** * 根据文件信息获取对应的图标 * @param {Object} fileInfo - 文件信息对象 * @param {boolean} fileInfo.is_dir - 是否为目录 * @param {string} fileInfo.name - 文件名 * @returns {string} 文件图标(emoji) * * @example * getFileIcon({ is_dir: true }) // "📁" * getFileIcon({ is_dir: false, name: 'image.png' }) // "🖼️" * getFileIcon({ is_dir: false, name: 'document.pdf' }) // "📕" */ export function getFileIcon(fileInfo) { if (!fileInfo) return FILE_ICONS.FILE // 如果是目录 if (fileInfo.is_dir) { return FILE_ICONS.FOLDER } // 获取文件扩展名 const ext = getExt(fileInfo.name) // 从映射表中查找图标 return FILE_ICON_MAP.get(ext) || FILE_ICONS.FILE } /** * 规范化文件路径(将反斜杠转换为正斜杠,并进行URL编码) * @param {string} path - 原始路径 * @param {boolean} encode - 是否进行URL编码(用于URL路径) * @returns {string} 规范化后的路径 * * @example * normalizeFilePath('C:\\Users\\user\\file.txt') // "C:/Users/user/file.txt" * normalizeFilePath('/home/user/file.txt') // "/home/user/file.txt" * normalizeFilePath('E:/中文路径/file.pdf', true) // "E:/%E4%B8%AD%E6%96%87%E8%B7%AF%E5%BE%84/file.pdf" */ export function normalizeFilePath(path, encode = false) { if (!path) return '' const normalized = normalizePathSeparators(path) // 如果需要编码,则使用 encodeURIComponent if (encode) { const parts = normalized.split('/') // 只对包含需要编码字符的路径段进行编码 // Windows 路径格式: E:/path/to/file,第一部分是盘符(如 E:),不应编码 return parts.map((segment, index) => { // 盘符部分(如 "E:")不编码 if (index === 0 && /^[A-Za-z]:$/.test(segment)) { return segment } // 检查是否需要编码(包含非ASCII字符或特殊字符) if (/[^A-Za-z0-9\-_.~]/.test(segment)) { return encodeURIComponent(segment) } return segment }).join('/') } return normalized } /** * 获取路径使用的分隔符(Windows 反斜杠或 Unix 正斜杠) * @param {string} path - 文件路径 * @returns {string} 分隔符 '\\' 或 '/' */ export function getPathSeparator(path) { return path.includes('\\') ? '\\' : '/' } /** * 获取父目录路径 * @param {string} path - 文件路径 * @returns {string} 父目录路径 * * @example * getParentPath('/home/user/docs/file.txt') // "/home/user/docs" * getParentPath('/home/user/docs/') // "/home/user" */ export function getParentPath(path) { if (!path) return '' const normalizedPath = normalizePathSeparators(path) const lastSlashIndex = normalizedPath.lastIndexOf('/') if (lastSlashIndex <= 0) { if (/^[A-Za-z]:$/.test(normalizedPath)) { return normalizedPath + '/' } return normalizedPath || '/' } const parentPath = normalizedPath.substring(0, lastSlashIndex) // 盘符根目录下文件:E:/file.txt → E:/ if (/^[A-Za-z]:$/.test(parentPath)) { return parentPath + '/' } return parentPath || '/' } /** * 文件列表排序:文件夹优先,支持多字段排序 * @param {Array} fileList - 文件列表 * @param {Object} options - 排序选项 { sortBy, sortOrder } * @returns {Array} 排序后的文件列表 * * @example * sortFileList(fileList, { sortBy: 'name', sortOrder: 'asc' }) */ /** * 格式化文件修改时间 * @param {string} t - 时间字符串 * @returns {string} 格式化后的时间,如 2026/04/11 14:30 */ export function formatFileTime(t) { if (!t) return '' const d = new Date(t) if (isNaN(d.getTime())) return t const pad = n => String(n).padStart(2, '0') return `${d.getFullYear()}/${pad(d.getMonth()+1)}/${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}` } export function sortFileList(fileList, options = {}) { if (!Array.isArray(fileList)) return fileList const { sortBy = 'name', sortOrder = 'asc' } = options const dir = sortOrder === 'desc' ? 1 : -1 return fileList.sort((a, b) => { // 文件夹始终排在前面 if (a.isDir !== b.isDir) { return a.isDir ? -1 : 1 } let cmp = 0 switch (sortBy) { case 'size': cmp = (a.size || 0) - (b.size || 0) break case 'type': { cmp = getExt(a.name).localeCompare(getExt(b.name)) break } case 'modified_time': { const ta = a.modified_time ? new Date(a.modified_time).getTime() : 0 const tb = b.modified_time ? new Date(b.modified_time).getTime() : 0 cmp = ta - tb break } default: // name cmp = a.name.localeCompare(b.name) } return cmp * dir }) }