Private
Public Access
1
0

优化:代码审查

清理:
- 删除重复的 composables(useFilePreview.js、useFileEdit.js)
- 已有 TypeScript 版本在 FileSystem/composables/

优化:
- 统一 API 层错误日志到 debugLog(system.ts)
- 移除 UpdatePanel 调试面板和调试文本

代码质量:
- 提升代码可维护性
- 统一错误处理方式
This commit is contained in:
2026-02-05 00:28:26 +08:00
parent f7d648ea52
commit 9eb39fbb8f
4 changed files with 8 additions and 991 deletions

View File

@@ -3,6 +3,7 @@
*/ */
import type { SystemInfo, CPU, Memory, Disk, File } from './types' import type { SystemInfo, CPU, Memory, Disk, File } from './types'
import { debugError } from '@/utils/debugLog'
/** /**
* 转换后端文件数据格式(蛇形 → 驼峰) * 转换后端文件数据格式(蛇形 → 驼峰)
@@ -162,7 +163,7 @@ export async function listZipContents(zipPath: string): Promise<File[]> {
const result = await window.go.main.App.ListZipContents(zipPath) const result = await window.go.main.App.ListZipContents(zipPath)
return transformFileList(result) return transformFileList(result)
} catch (error) { } catch (error) {
console.error('[API] listZipContents 错误:', error) debugError('[API] listZipContents 错误:', error)
throw error throw error
} }
} }
@@ -178,7 +179,7 @@ export async function extractFileFromZip(zipPath: string, filePath: string): Pro
const result = await window.go.main.App.ExtractFileFromZip(zipPath, filePath) const result = await window.go.main.App.ExtractFileFromZip(zipPath, filePath)
return result return result
} catch (error) { } catch (error) {
console.error('[API] extractFileFromZip 错误:', error) debugError('[API] extractFileFromZip 错误:', error)
throw error throw error
} }
} }
@@ -195,7 +196,7 @@ export async function extractFileFromZipToTemp(zipPath: string, filePath: string
const result = await window.go.main.App.ExtractFileFromZipToTemp(zipPath, filePath) const result = await window.go.main.App.ExtractFileFromZipToTemp(zipPath, filePath)
return result return result
} catch (error) { } catch (error) {
console.error('[API] extractFileFromZipToTemp 错误:', error) debugError('[API] extractFileFromZipToTemp 错误:', error)
throw error throw error
} }
} }
@@ -211,7 +212,7 @@ export async function getZipFileInfo(zipPath: string, filePath: string): Promise
const result = await window.go.main.App.GetZipFileInfo(zipPath, filePath) const result = await window.go.main.App.GetZipFileInfo(zipPath, filePath)
return transformFile(result) return transformFile(result)
} catch (error) { } catch (error) {
console.error('[API] getZipFileInfo 错误:', error) debugError('[API] getZipFileInfo 错误:', error)
throw error throw error
} }
} }
@@ -226,7 +227,7 @@ export async function openPath(path: string): Promise<void> {
try { try {
await window.go.main.App.OpenPath(path) await window.go.main.App.OpenPath(path)
} catch (error) { } catch (error) {
console.error('[API] openPath 错误:', error) debugError('[API] openPath 错误:', error)
throw error throw error
} }
} }
@@ -259,7 +260,7 @@ export async function resolveShortcut(lnkPath: string): Promise<{
const result = await window.go.main.App.ResolveShortcut(lnkPath) const result = await window.go.main.App.ResolveShortcut(lnkPath)
return result return result
} catch (error) { } catch (error) {
console.error('[API] resolveShortcut 错误:', error) debugError('[API] resolveShortcut 错误:', error)
throw error throw error
} }
} }
@@ -280,7 +281,7 @@ export async function detectFileTypeByContent(path: string): Promise<{
const result = await window.go.main.App.DetectFileTypeByContent(path) const result = await window.go.main.App.DetectFileTypeByContent(path)
return result as any return result as any
} catch (error) { } catch (error) {
console.error('[API] detectFileTypeByContent 错误:', error) debugError('[API] detectFileTypeByContent 错误:', error)
throw error throw error
} }
} }

View File

@@ -79,20 +79,8 @@
</div> </div>
</a-alert> </a-alert>
<!-- 调试信息始终显示 -->
<div style="font-size: 12px; color: #999; padding: 8px; background: var(--color-fill-2); margin-top: 16px; border-radius: 4px;">
<strong>调试信息</strong>
<br>downloading = {{ downloading }}
<br>downloadProgress = {{ downloadProgress }}
<br>downloadStatus = {{ downloadStatus }}
<br>progressInfo = {{ progressInfo }}
</div>
<!-- 下载进度 --> <!-- 下载进度 -->
<div v-if="downloadProgress > 0 || downloading" class="download-progress"> <div v-if="downloadProgress > 0 || downloading" class="download-progress">
<div style="font-size: 11px; color: #999; margin-bottom: 8px;">
进度条已显示downloadProgress={{ downloadProgress }}, downloading={{ downloading }}
</div>
<a-progress <a-progress
:percent="downloadProgress" :percent="downloadProgress"
:status="downloadStatus" :status="downloadStatus"

View File

@@ -1,369 +0,0 @@
/**
* 文件编辑和保存逻辑 composable
*
* @module composables/useFileEdit
* @description 封装文件编辑、保存、草稿管理等逻辑
*/
import { ref, computed, watch } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import { STORAGE_KEYS } from '@/utils/constants'
/**
* 草稿存储键
*/
const DRAFT_STORAGE_KEY = 'filesystem_draft_content'
/**
* 文件编辑 composable
* @param {Object} options - 配置选项
* @param {Ref<string>} options.filePath - 当前文件路径
* @param {Ref<string>} options.fileContent - 文件内容
* @param {Function} options.onWriteFile - 写入文件的函数
* @param {Function} options.onReset - 重置内容的函数
* @returns {UseFileEditReturn} 文件编辑操作 API
*/
export function useFileEdit(options = {}) {
const {
filePath,
fileContent,
onWriteFile,
onReset,
} = options
// ========== 编辑状态 ==========
/**
* 是否正在保存
* @type {Ref<boolean>}
*/
const isSaving = ref(false)
/**
* 是否是快捷键触发的保存
* @type {Ref<boolean>}
*/
const isShortcutSave = ref(false)
/**
* 保存成功提示消息
* @type {Ref<string>}
*/
const saveSuccessMessage = ref('')
/**
* 原始文件内容(用于检测变更)
* @type {Ref<string>}
*/
const originalContent = ref('')
/**
* 是否为编辑模式
* @type {Ref<boolean>}
*/
const isEditMode = ref(localStorage.getItem(STORAGE_KEYS.FILESYSTEM.EDIT_MODE) === 'true')
// ========== 计算属性 ==========
/**
* 文件内容是否已修改
*/
const isFileModified = computed(() => {
return originalContent.value !== undefined &&
originalContent.value !== fileContent.value
})
/**
* 内容是否发生变化(用于按钮禁用判断)
*/
const contentChanged = computed(() => {
return fileContent.value !== '' &&
fileContent.value !== originalContent.value
})
/**
* 是否可以保存文件
*/
const canSaveFile = computed(() => {
return isEditMode.value && contentChanged.value
})
/**
* 是否可以重置内容
*/
const canResetContent = computed(() => {
return isEditMode.value &&
contentChanged.value &&
originalContent.value !== undefined
})
// ========== 草稿管理 ==========
/**
* 保存草稿到 localStorage
*/
const saveDraft = () => {
try {
const draft = {
content: fileContent.value,
path: filePath.value,
timestamp: Date.now(),
}
localStorage.setItem(DRAFT_STORAGE_KEY, JSON.stringify(draft))
localStorage.setItem(DRAFT_STORAGE_KEY + '_time', Date.now().toString())
} catch (error) {
console.warn('[saveDraft] 保存草稿失败:', error)
}
}
/**
* 清除草稿
*/
const clearDraft = () => {
try {
localStorage.removeItem(DRAFT_STORAGE_KEY)
localStorage.removeItem(DRAFT_STORAGE_KEY + '_time')
} catch (error) {
console.warn('[clearDraft] 清除草稿失败:', error)
}
}
/**
* 加载草稿
* @returns {Object|null} 草稿数据
*/
const loadDraft = () => {
try {
const draftStr = localStorage.getItem(DRAFT_STORAGE_KEY)
if (!draftStr) return null
const draft = JSON.parse(draftStr)
// 检查草稿是否过期24小时
const timeStr = localStorage.getItem(DRAFT_STORAGE_KEY + '_time')
if (timeStr) {
const time = parseInt(timeStr, 10)
const now = Date.now()
const hours = (now - time) / (1000 * 60 * 60)
if (hours > 24) {
clearDraft()
return null
}
}
return draft
} catch (error) {
console.warn('[loadDraft] 加载草稿失败:', error)
return null
}
}
// ========== 保存操作 ==========
/**
* 显示手动保存对话框
* @param {boolean} isShortcut - 是否是快捷键触发
*/
const showManualSaveDialog = (isShortcut) => {
isShortcutSave.value = isShortcut
Modal.confirm({
title: '保存文件',
content: `确定要保存文件 ${filePath.value} 吗?`,
okText: '保存',
cancelText: '取消',
onOk: () => {
saveToFile(filePath.value, getFileName(filePath.value), isShortcut)
},
})
}
/**
* 保存到文件
* @param {string} targetPath - 目标路径
* @param {string} fileName - 文件名
* @param {boolean} isShortcut - 是否是快捷键触发
* @returns {Promise<boolean>} 是否成功
*/
const saveToFile = async (targetPath, fileName, isShortcut) => {
isSaving.value = true
try {
const success = await onWriteFile(fileContent.value, targetPath, fileName, isShortcut)
if (success) {
originalContent.value = fileContent.value
clearDraft()
}
return success
} finally {
isSaving.value = false
}
}
/**
* 处理保存内容
* @returns {Promise<boolean>} 是否成功
*/
const handleSaveContent = async () => {
if (!canSaveFile.value) {
return false
}
return await saveToFile(filePath.value, getFileName(filePath.value), false)
}
/**
* 另存为
*/
const handleSaveAs = async () => {
try {
// 简单实现:使用 prompt 获取路径
const targetPath = prompt('请输入保存路径:', filePath.value)
if (!targetPath) {
return false
}
const fileName = getFileName(targetPath)
return await saveToFile(targetPath, fileName, false)
} catch (error) {
Message.error(`保存对话框失败: ${error.message || error}`)
return false
}
}
/**
* 处理写入文件(快捷键或按钮)
* @param {boolean} isShortcut - 是否是快捷键触发
* @returns {Promise<boolean>} 是否成功
*/
const handleWriteFile = async (isShortcut = false) => {
if (!fileContent.value || !filePath.value) {
Message.warning('没有可保存的内容')
return false
}
// 如果内容未修改,快捷键保存时静默返回
if (!isFileModified.value && isShortcut) {
return false
}
// 快捷键:静默保存
if (isShortcut) {
return await saveToFile(filePath.value, getFileName(filePath.value), true)
}
// 按钮:显示确认对话框
showManualSaveDialog(false)
return false
}
// ========== 重置操作 ==========
/**
* 重置内容到原始状态
*/
const resetContent = () => {
if (onReset) {
onReset()
} else {
fileContent.value = originalContent.value
}
}
// ========== 编辑模式切换 ==========
/**
* 切换编辑模式
*/
const toggleEditMode = () => {
isEditMode.value = !isEditMode.value
// 持久化
try {
localStorage.setItem(STORAGE_KEYS.FILESYSTEM.EDIT_MODE, isEditMode.value.toString())
} catch (e) {
console.warn('[toggleEditMode] 保存编辑模式失败:', e)
}
// 进入编辑模式时,记录原始内容
if (isEditMode.value) {
originalContent.value = fileContent.value
}
}
// ========== 工具函数 ==========
/**
* 从路径获取文件名
* @param {string} path - 文件路径
* @returns {string} 文件名
*/
const getFileName = (path) => {
if (!path) return ''
const parts = path.split(/[/\\]/)
return parts[parts.length - 1] || path
}
// ========== 监听内容变化 ==========
/**
* 监听文件内容变化,自动保存草稿
*/
watch(fileContent, () => {
if (fileContent.value && fileContent.value !== originalContent.value) {
saveDraft()
}
})
/**
* 监听文件路径变化,更新原始内容
*/
watch(filePath, () => {
originalContent.value = fileContent.value
})
return {
// 状态
isSaving,
isShortcutSave,
saveSuccessMessage,
originalContent,
isEditMode,
isFileModified,
canSaveFile,
canResetContent,
// 方法
saveDraft,
clearDraft,
loadDraft,
handleSaveContent,
handleSaveAs,
handleWriteFile,
resetContent,
toggleEditMode,
}
}
/**
* @typedef {Object} UseFileEditReturn
* @property {Ref<boolean>} isSaving - 是否正在保存
* @property {Ref<boolean>} isShortcutSave - 是否是快捷键触发
* @property {Ref<string>} saveSuccessMessage - 保存成功提示消息
* @property {Ref<string>} originalContent - 原始文件内容
* @property {Ref<boolean>} isEditMode - 是否为编辑模式
* @property {ComputedRef<boolean>} isFileModified - 文件内容是否已修改
* @property {ComputedRef<boolean>} canSaveFile - 是否可以保存文件
* @property {ComputedRef<boolean>} canResetContent - 是否可以重置内容
* @property {Function} saveDraft - 保存草稿
* @property {Function} clearDraft - 清除草稿
* @property {Function} loadDraft - 加载草稿
* @property {Function} handleSaveContent - 处理保存内容
* @property {Function} handleSaveAs - 另存为
* @property {Function} handleWriteFile - 处理写入文件
* @property {Function} resetContent - 重置内容
* @property {Function} toggleEditMode - 切换编辑模式
*/

View File

@@ -1,603 +0,0 @@
/**
* 文件预览逻辑 composable
*
* @module composables/useFilePreview
* @description 封装文件预览、HTML/Markdown 渲染、二进制文件信息显示等逻辑
*/
import { ref, computed } from 'vue'
import { marked } from '@/utils/markedExtensions'
import { FILE_EXTENSIONS, FILE_SIZE_THRESHOLDS } from '@/utils/constants'
import { getExt } from '@/utils/fileHelpers'
import { isOfficeFile } from '@/utils/fileTypeHelpers'
import { debugLog, debugWarn, debugError } from '@/utils/debugLog'
/**
* 文件预览 composable
* @param {Object} options - 配置选项
* @param {Ref<string>} options.filePath - 当前文件路径
* @param {Ref<string>} options.fileContent - 文件内容
* @param {Ref<Array>} options.fileList - 文件列表
* @param {Function} options.onReadFile - 读取文件的函数
* @returns {UseFilePreviewReturn} 文件预览操作 API
*/
export function useFilePreview(options = {}) {
const {
filePath,
fileContent,
fileList,
onReadFile,
} = options
// ========== 预览状态 ==========
/**
* 预览 URL
* @type {Ref<string>}
*/
const previewUrl = ref('')
/**
* 文件服务器URL
* @type {Ref<string>}
*/
const fileServerURL = ref('http://localhost:18765')
/**
* 渲染后的 HTML/Markdown 内容
* @type {Ref<string>}
*/
const rendered = ref('')
/**
* 图片加载状态
* @type {Ref<boolean>}
*/
const imageLoading = ref(false)
/**
* 图片宽度
* @type {Ref<number>}
*/
const imageWidth = ref(0)
/**
* 图片高度
* @type {Ref<number>}
*/
const imageHeight = ref(0)
/**
* 是否显示图片预览
* @type {Ref<boolean>}
*/
const isImageView = ref(false)
/**
* 是否显示视频预览
* @type {Ref<boolean>}
*/
const isVideoView = ref(false)
/**
* 是否显示音频预览
* @type {Ref<boolean>}
*/
const isAudioView = ref(false)
/**
* 是否为 PDF 文件
* @type {Ref<boolean>}
*/
const isPdfFile = ref(false)
/**
* 是否为 HTML 文件
* @type {Ref<boolean>}
*/
const isHtmlFile = ref(false)
/**
* 是否为 Markdown 文件
* @type {Ref<boolean>}
*/
const isMarkdownFile = ref(false)
/**
* 是否为二进制文件信息展示
* @type {Ref<boolean>}
*/
const isBinaryFile = ref(false)
/**
* HTML 预览的 blob URL
* @type {Ref<string>}
*/
const htmlPreviewUrl = ref('')
// ========== 计算属性 ==========
/**
* 当前文件名
*/
const currentFileName = computed(() => {
if (!filePath.value) return ''
const pathStr = typeof filePath.value === 'string' ? filePath.value : String(filePath.value || '')
const parts = pathStr.split(/[/\\]/)
return parts[parts.length - 1]
})
/**
* 当前文件完整路径
*/
const currentFileFullPath = computed(() => filePath.value || '')
/**
* 当前图片尺寸
*/
const currentImageDimensions = computed(() => {
if (!imageWidth.value || !imageHeight.value) return ''
return `${imageWidth.value}×${imageHeight.value}`
})
// ========== 图片预览 ==========
/**
* 预览图片
* @param {string} targetPath - 目标路径
*/
const previewImage = async (targetPath) => {
const pathToPreview = targetPath || filePath.value
if (!pathToPreview) return
resetPreviewState()
const ext = getExt(pathToPreview)
if (!FILE_EXTENSIONS.IMAGE.includes(ext)) {
return
}
imageLoading.value = true
isImageView.value = true
// 构建预览 URL
const encodedPath = encodeURIComponent(pathToPreview)
previewUrl.value = `${fileServerURL.value}/file?path=${encodedPath}`
}
/**
* 图片加载成功回调
* @param {Event} e - 加载事件
*/
const onImageLoad = (e) => {
imageLoading.value = false
imageWidth.value = e.naturalWidth || e.target?.width || 0
imageHeight.value = e.naturalHeight || e.target?.height || 0
}
/**
* 图片加载失败回调
*/
const onImageError = () => {
imageLoading.value = false
debugWarn('[onImageError] 图片加载失败')
}
// ========== 视频/音频/PDF 预览 ==========
/**
* 预览媒体文件(视频/音频/PDF
* @param {string} mediaType - 媒体类型 ('video' | 'audio' | 'pdf')
* @param {string} targetPath - 目标路径
*/
const previewMedia = (mediaType, targetPath) => {
const pathToPreview = targetPath || filePath.value
if (!pathToPreview) return
resetPreviewState()
const encodedPath = encodeURIComponent(pathToPreview)
previewUrl.value = `${fileServerURL.value}/file?path=${encodedPath}`
if (mediaType === 'video') {
isVideoView.value = true
} else if (mediaType === 'audio') {
isAudioView.value = true
} else if (mediaType === 'pdf') {
isPdfFile.value = true
}
}
/**
* 预览视频
* @param {string} targetPath - 目标路径
*/
const previewVideo = (targetPath) => previewMedia('video', targetPath)
/**
* 预览音频
* @param {string} targetPath - 目标路径
*/
const previewAudio = (targetPath) => previewMedia('audio', targetPath)
/**
* 预览 PDF
* @param {string} targetPath - 目标路径
*/
const previewPdf = (targetPath) => previewMedia('pdf', targetPath)
// ========== HTML 预览 ==========
/**
* 提取 HTML 文件中的样式
* @param {string} htmlContent - HTML 内容
* @param {string} basePath - 基础路径
* @returns {Promise<string>} 提取的 CSS 样式
*/
const extractHtmlStyles = async (htmlContent, basePath) => {
const linkRegex = /<link[^>]*href=(["'])([^"']+)\1[^>]*>/gi
const links = [...htmlContent.matchAll(linkRegex)]
if (links.length === 0) return ''
let linkCount = 0
const styles = []
for (const match of links) {
const linkTag = match[0]
const hrefMatch = match[2]?.match(/^https?:\/\//i)
const fullTag = match[0]
const href = match[2]
debugLog(`[extractHtmlStyles] 发现第 ${linkCount} 个 link 标签:`, fullTag)
const cssPath = href?.replace(/^\.\//, '').replace(/^\//, '')
debugLog('[extractHtmlStyles] 解析后 CSS 路径:', cssPath)
if (hrefMatch) {
debugLog('[extractHtmlStyles] 跳过外部 CSS:', hrefMatch[1])
continue
}
debugLog('[extractHtmlStyles] 正在读取 CSS 文件:', cssPath)
try {
// 从 HTML 文件所在目录读取 CSS
const cssFullPath = basePath + '/' + cssPath
const cssContent = await onReadFile(cssFullPath)
if (cssContent) {
const cssSize = cssContent.length
debugLog(`[extractHtmlStyles] 成功读取并转换 CSS: ${cssSize} 字符`)
// 转换 CSS 中的 URL 为 base64
const convertedCss = await convertCssUrls(cssContent, basePath)
styles.push(convertedCss)
}
} catch (error) {
debugWarn('[extractHtmlStyles] 无法读取 CSS:', cssPath, error.message)
}
linkCount++
}
debugLog(`处理完成: 找到 ${linkCount} 个 link 标签, 成功提取 ${styles.length} 个 CSS 文件`)
debugLog(`提取的 CSS 总大小: ${styles.join('\n\n').length} 字符`)
return styles.join('\n\n')
}
/**
* 转换 CSS 中的相对 URL 为 base64
* @param {string} css - CSS 内容
* @param {string} basePath - 基础路径
* @returns {Promise<string>} 转换后的 CSS
*/
const convertCssUrls = async (css, basePath) => {
const urlRegex = /url\((["']?)([^"')]+)\1\)/gi
return css.replace(urlRegex, async (match, quote, url) => {
// 跳过 data: URLs 和绝对 URLs
if (url.startsWith('data:') || /^https?:\/\//i.test(url)) {
return match
}
try {
const imagePath = basePath + '/' + url.replace(/^\.\//, '')
const base64 = await fileToBase64(imagePath)
debugLog(`[convertCssUrls] ${url} -> base64`)
return `url("data:image/${getExt(imagePath)};base64,${base64}")`
} catch (err) {
debugWarn('[convertCssUrls] 失败:', imagePath, err.message)
return match
}
})
}
/**
* 将文件转换为 base64
* @param {string} filePath - 文件路径
* @returns {Promise<string>} base64 字符串
*/
const fileToBase64 = async (filePath) => {
// 这里需要调用实际的文件读取 API
// 简化实现,返回空字符串
return ''
}
/**
* 预览 HTML 文件
* @param {string} targetPath - 目标路径
*/
const previewHtml = async (targetPath) => {
const pathToPreview = targetPath || filePath.value
if (!pathToPreview) return
resetPreviewState()
isHtmlFile.value = true
debugLog('开始处理 CSS')
debugLog('HTML 文件路径:', pathToPreview)
const basePath = pathToPreview.replace(/[^/\\]+$/, '')
try {
let htmlContent = fileContent.value
// 提取并转换 CSS
const styles = await extractHtmlStyles(htmlContent, basePath)
// 转换图片引用
const imgRegex = /<img[^>]*src=(["'])([^"']+)\1[^>]*>/gi
htmlContent = htmlContent.replace(imgRegex, (match, quote, src) => {
// 跳过 data: URLs 和绝对 URLs
if (src.startsWith('data:') || /^https?:\/\//i.test(src)) {
return match
}
debugLog(`[previewHtml] ${src} -> base64`)
// 转换为绝对路径
const imagePath = basePath + src.replace(/^\.\//, '').replace(/^\//, '')
// 简化实现:使用 fileServerURL
const encodedPath = encodeURIComponent(imagePath)
const newSrc = `${fileServerURL.value}/file?path=${encodedPath}`
return match.replace(src, newSrc)
})
// 移除本地脚本
htmlContent = htmlContent.replace(/<script[^>]*src=(["'])[^"']+\1[^>]*>/gi, (match, quote, src) => {
const srcMatch = match.match(/src=(["'])([^"']+)\1/i)
if (srcMatch) {
const srcValue = srcMatch[2]
if (!srcValue.startsWith('http')) {
debugLog(`[previewHtml] 移除本地脚本: ${srcValue}`)
return ''
}
}
return match
})
// 清理遗漏的 CSS 链接
htmlContent = htmlContent.replace(/<link[^>]*rel=(["'])stylesheet\1[^>]*>/gi, (match) => {
const hrefMatch = match.match(/href=(["'])([^"']+)\1/i)
if (hrefMatch && !/^https?:\/\//i.test(hrefMatch[2])) {
debugLog(`[previewHtml] 清理遗漏的CSS链接: ${hrefMatch[2]}`)
return ''
}
return match
})
// 构建最终 HTML
const finalHtml = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>${styles}</style>
</head>
<body>
${htmlContent}
</body>
</html>
`
// 创建 blob URL
const blob = new Blob([finalHtml], { type: 'text/html' })
htmlPreviewUrl.value = URL.createObjectURL(blob)
rendered.value = finalHtml
} catch (error) {
debugError('[previewHtml] 处理失败:', error)
}
}
// ========== Markdown 预览 ==========
/**
* 预览 Markdown 文件
* @param {string} targetPath - 目标路径
*/
const previewMarkdown = async (targetPath) => {
const pathToPreview = targetPath || filePath.value
if (!pathToPreview) return
resetPreviewState()
isMarkdownFile.value = true
try {
renderMarkdown(fileContent.value)
} catch (error) {
debugError('[renderMarkdown] 解析失败:', error)
}
}
/**
* 渲染 Markdown
* @param {string} markdown - Markdown 内容
*/
const renderMarkdown = (markdown) => {
try {
rendered.value = marked(markdown)
} catch (error) {
debugError('[renderMarkdown] 解析失败:', error)
rendered.value = '<p class="error">Markdown 解析失败</p>'
}
}
// ========== 二进制文件信息 ==========
/**
* 获取字符串显示宽度(用于对齐)
* @param {string} str - 字符串
* @returns {number} 显示宽度
*/
const getDisplayWidth = (str) => {
let width = 0
for (const char of str) {
if (char.match(/[\u4e00-\u9fa5]/)) {
width += 2
} else {
width += 1
}
}
return width
}
/**
* 按显示宽度填充
* @param {string} str - 字符串
* @param {number} targetWidth - 目标宽度
* @returns {string} 填充后的字符串
*/
const padByDisplayWidth = (str, targetWidth) => {
const currentWidth = getDisplayWidth(str)
const padding = Math.max(0, targetWidth - currentWidth)
return str + ' '.repeat(padding)
}
/**
* 显示二进制文件信息
* @param {string} ext - 文件扩展名
* @param {string} filePathParam - 文件路径
*/
const showBinaryFileInfo = (ext, filePathParam) => {
resetPreviewState()
isBinaryFile.value = true
const file = fileList.value.find(f => f.path === filePathParam)
if (!file) return
const extUpper = ext.toUpperCase()
const extPadded = padByDisplayWidth(extUpper, 6)
const sizeMB = (file.size / 1024 / 1024).toFixed(2)
const sizeStr = `${sizeMB} MB`.padStart(10, ' ')
rendered.value = `
<div class="binary-file-info">
<p>
<span class="file-type">${extPadded} 文件</span>
<span class="file-size">${sizeStr}</span>
</p>
<p class="file-name">${file.name}</p>
</div>
`
}
// ========== 工具函数 ==========
/**
* 重置预览状态
*/
const resetPreviewState = () => {
isImageView.value = false
isVideoView.value = false
isAudioView.value = false
isPdfFile.value = false
isHtmlFile.value = false
isMarkdownFile.value = false
isBinaryFile.value = false
if (htmlPreviewUrl.value) {
URL.revokeObjectURL(htmlPreviewUrl.value)
htmlPreviewUrl.value = ''
}
previewUrl.value = ''
rendered.value = ''
imageWidth.value = 0
imageHeight.value = 0
}
return {
// 状态
previewUrl,
fileServerURL,
rendered,
imageLoading,
imageWidth,
imageHeight,
isImageView,
isVideoView,
isAudioView,
isPdfFile,
isHtmlFile,
isMarkdownFile,
isBinaryFile,
htmlPreviewUrl,
currentFileName,
currentFileFullPath,
currentImageDimensions,
// 方法
previewImage,
previewVideo,
previewAudio,
previewPdf,
previewHtml,
previewMarkdown,
renderMarkdown,
showBinaryFileInfo,
onImageLoad,
onImageError,
isOfficeFile,
resetPreviewState,
}
}
/**
* @typedef {Object} UseFilePreviewReturn
* @property {Ref<string>} previewUrl - 预览 URL
* @property {Ref<string>} fileServerURL - 文件服务器URL
* @property {Ref<string>} rendered - 渲染后的内容
* @property {Ref<boolean>} imageLoading - 图片加载状态
* @property {Ref<number>} imageWidth - 图片宽度
* @property {Ref<number>} imageHeight - 图片高度
* @property {Ref<boolean>} isImageView - 是否显示图片预览
* @property {Ref<boolean>} isVideoView - 是否显示视频预览
* @property {Ref<boolean>} isAudioView - 是否显示音频预览
* @property {Ref<boolean>} isPdfFile - 是否为 PDF 文件
* @property {Ref<boolean>} isHtmlFile - 是否为 HTML 文件
* @property {Ref<boolean>} isMarkdownFile - 是否为 Markdown 文件
* @property {Ref<boolean>} isBinaryFile - 是否为二进制文件信息展示
* @property {Ref<string>} htmlPreviewUrl - HTML 预览的 blob URL
* @property {ComputedRef<string>} currentFileName - 当前文件名
* @property {ComputedRef<string>} currentFileFullPath - 当前文件完整路径
* @property {ComputedRef<string>} currentImageDimensions - 当前图片尺寸
* @property {Function} previewImage - 预览图片
* @property {Function} previewVideo - 预览视频
* @property {Function} previewAudio - 预览音频
* @property {Function} previewPdf - 预览 PDF
* @property {Function} previewHtml - 预览 HTML
* @property {Function} previewMarkdown - 预览 Markdown
* @property {Function} renderMarkdown - 渲染 Markdown
* @property {Function} showBinaryFileInfo - 显示二进制文件信息
* @property {Function} onImageLoad - 图片加载成功回调
* @property {Function} onImageError - 图片加载失败回调
* @property {Function} isOfficeFile - 判断是否为 Office 文件
* @property {Function} resetPreviewState - 重置预览状态
*/