优化:代码审查
清理: - 删除重复的 composables(useFilePreview.js、useFileEdit.js) - 已有 TypeScript 版本在 FileSystem/composables/ 优化: - 统一 API 层错误日志到 debugLog(system.ts) - 移除 UpdatePanel 调试面板和调试文本 代码质量: - 提升代码可维护性 - 统一错误处理方式
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 - 切换编辑模式
|
|
||||||
*/
|
|
||||||
@@ -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 - 重置预览状态
|
|
||||||
*/
|
|
||||||
Reference in New Issue
Block a user