PDF,HTML,md格式文件在线查看工具
VUE3 实现了 PDF,HTML,md格式文件在线查看工具
在线体验地址: http://114.55.230.54/
实现了一款漂亮的PDF,HTML,md格式文件在线查看网页工具
1、PDF预览
1.1 实现代码
<script setup>
import { ref, watch, computed } from 'vue'// 状态管理
const files = ref([]) // 存储上传的HTML文件列表
const activeFileIndex = ref(-1) // 当前选中的文件索引
const viewMode = ref('preview') // 预览模式:'preview'(渲染预览)或 'code'(源代码)
const showHelp = ref(false) // 帮助提示框显示状态// 计算属性:当前选中的文件
const activeFile = computed(() => {return activeFileIndex.value >= 0 ? files.value[activeFileIndex.value] : null
})// 监听选中文件索引变化,自动滚动到可视区域
watch(activeFileIndex, (newIndex) => {if (newIndex >= 0) {const fileItems = document.querySelectorAll('.file-item')if (fileItems[newIndex]) {fileItems[newIndex].scrollIntoView({ behavior: 'smooth', block: 'nearest' })}}
})// 处理文件上传
const handleFileUpload = (e) => {if (!e.target.files) returnfor (let i = 0; i < e.target.files.length; i++) {let file = e.target.files[i]if (!file.name.endsWith('.pdf') && !file.name.endsWith('.PDF')) {alert('请上传扩展名为.PDF的文件')continue}// 避免重复上传同名文件// const isDuplicate = files.value.some(item => item.name === file.name)// if (isDuplicate) {// alert(`文件 "${file.name}" 已存在,请选择其他文件或删除现有文件后重新上传`)// return// }// 生成本地 URLconst blobUrl = URL.createObjectURL(file)// 添加文件到列表files.value.push({name: file.name,content: blobUrl,size: formatFileSize(file.size)})activeFileIndex.value = files.value.length - 1}// 重置文件输入框e.target.value = ''
}// 移除指定索引的文件
const removeFile = (index) => {const fileToDelete = files.value[index]if (confirm(`确定要删除文件 "${fileToDelete.name}" 吗?删除后无法恢复`)) {// 从列表中删除文件files.value.splice(index, 1)// 处理选中状态if (index === activeFileIndex.value) {activeFileIndex.value = files.value.length > 0 ? 0 : -1} else if (index < activeFileIndex.value) {activeFileIndex.value--}}
}// 工具函数:格式化文件大小(B → KB/MB)
const formatFileSize = (bytes) => {if (bytes < 1024) return `${bytes} B`if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
</script><template><div class="app-container"><!-- 顶部导航栏 --><header class="app-header"><div class="header-inner"><div class="logo-group"><i class="fa fa-html5"></i><h1>PDF文件浏览器</h1></div><div class="header-actions"><button class="help-btn" @click="showHelp = !showHelp"><i class="fa fa-question-circle"></i><span class="help-text">帮助</span></button></div></div></header><!-- 帮助提示框 --><div class="help-container" v-if="showHelp"><div class="help-inner"><div class="help-text-group"><h3>使用指南</h3><p>1. 点击"选择PDF文件"按钮选择文件</p><p>2. 在左侧文件列表中点击文件名查看内容</p><p>3. 可以删除已选择的文件</p></div><button class="close-help-btn" @click="showHelp = false"><i class="fa fa-times"></i></button></div></div><!-- 主内容区 --><main class="app-main"><!-- 左侧文件列表 --><div class="file-list-sidebar"><div class="upload-section"><label for="file-upload" class="upload-btn"><i class="fa fa-upload"></i>选择PDF文件</label><inputid="file-upload"type="file"multipleaccept=".pdf,.PDF"@change="handleFileUpload"class="file-input-hidden"></div><div class="file-list-wrapper"><div class="empty-file-state" v-if="files.length === 0"><i class="fa fa-file-code-o"></i><p>没有选择的文件</p><p class="empty-tip">点击上方按钮选择PDF文件</p></div><ul class="file-list" v-else><liv-for="(file, index) in files":key="index"class="file-item":class="{ 'active': activeFileIndex === index }"@click="activeFileIndex = index"><div class="file-info"><i class="fa fa-file-html-o"></i><span class="file-name">{{ file.name }}</span></div><buttonclass="delete-file-btn"@click.stop="removeFile(index)"title="删除文件"><i class="fa fa-trash-o"></i></button></li></ul></div></div><!-- 右侧预览区 --><div class="preview-main"><div class="preview-header" v-if="activeFile"><h2 class="preview-file-name">{{ activeFile.name }}</h2><div class="view-mode-group"></div></div><div class="preview-content"><div class="empty-preview-state" v-if="!activeFile"><i class="fa fa-eye"></i><p>请从左侧选择一个文件进行预览</p></div><!-- 预览模式 --><div class="html-preview" v-if="activeFile && viewMode === 'preview'"><div class="preview-content-inner"><iframe width="100%" style="height: calc(100vh - 250px)" scrolling="no":src="`/document-file/pdf/web/viewer.html?file=${activeFile.content}`"></iframe></div></div></div></div></main><!-- 底部状态栏 --><footer class="app-footer"><div class="footer-inner"><div class="current-file-info"><span v-if="activeFile"><i class="fa fa-file-text-o"></i>{{ activeFile.name }}</span><span v-else>未选择文件</span></div><div class="file-count-info"><span>{{ files.length }} 个文件</span></div></div></footer></div>
</template><style scoped lang="scss">
// 基础变量定义
$color-primary: #3b82f6;
$color-primary-light: rgba(59, 130, 246, 0.1);
$color-primary-hover: rgba(59, 130, 246, 0.9);
$color-orange: #f97316;
$color-red: #ef4444;
$color-gray-50: #f9fafb;
$color-gray-100: #f3f4f6;
$color-gray-200: #e5e7eb;
$color-gray-400: #9ca3af;
$color-gray-500: #6b7280;
$color-gray-600: #4b5563;
$color-gray-700: #374151;
$color-dark: #1e293b;
$color-light: #f1f5f9;
$color-white: #ffffff;$shadow-base: 0 4px 20px rgba(0, 0, 0, 0.08);
$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);$border-radius: 0.5rem;
$transition-base: all 0.2s ease;$container-max-width: 1280px;
$sidebar-width-mobile: 100%;
$sidebar-width-desktop: 20rem;// 工具混合宏
@mixin flex-center {display: flex;align-items: center;justify-content: center;
}@mixin flex-between {display: flex;align-items: center;justify-content: space-between;
}@mixin text-ellipsis {white-space: nowrap;overflow: hidden;text-overflow: ellipsis;
}// 基础样式重置
* {margin: 0;padding: 0;box-sizing: border-box;
}// 根容器样式
.app-container {display: flex;flex-direction: column;height: calc(100vh - 90px);overflow: hidden;background-color: $color-gray-50;font-family: 'Inter', system-ui, sans-serif;color: #1f2937;
}// 顶部导航栏样式
.app-header {background-color: $color-white;box-shadow: $shadow-sm;z-index: 10;padding: 0.75rem 1rem;.header-inner {@include flex-between;max-width: $container-max-width;margin: 0 auto;}.logo-group {@include flex-center;gap: 0.5rem;i {color: $color-orange;font-size: 1.5rem;}h1 {font-size: 1.25rem;font-weight: 600;color: $color-primary;}}.header-actions {.help-btn {@include flex-center;gap: 0.25rem;background: transparent;border: none;color: $color-gray-600;cursor: pointer;font-size: 1rem;transition: $transition-base;&:hover {color: $color-primary;}.help-text {display: none;@media (min-width: 768px) {display: inline;}}}}
}// 帮助提示框样式
.help-container {background-color: rgba(59, 130, 246, 0.05);border-left: 4px solid $color-primary;padding: 1rem;box-shadow: $shadow-sm;transition: $transition-base;.help-inner {@include flex-between;align-items: flex-start;max-width: $container-max-width;margin: 0 auto;}.help-text-group {h3 {font-size: 1rem;font-weight: 600;color: $color-primary;margin-bottom: 0.5rem;}p {font-size: 0.875rem;color: $color-gray-600;margin-bottom: 0.25rem;}}.close-help-btn {background: transparent;border: none;color: $color-gray-500;cursor: pointer;font-size: 1rem;transition: $transition-base;&:hover {color: $color-gray-700;}}
}// 主内容区样式
.app-main {display: flex;flex: 1;overflow: hidden;
}// 左侧文件列表侧边栏
.file-list-sidebar {width: $sidebar-width-mobile;background-color: $color-white;border-right: 1px solid $color-gray-200;display: flex;flex-direction: column;height: 100%;@media (min-width: 768px) {width: $sidebar-width-desktop;}// 上传区域.upload-section {padding: 1rem;border-bottom: 1px solid $color-gray-200;.upload-btn {@include flex-center;gap: 0.5rem;display: block;width: 100%;padding: 0.5rem 1rem;background-color: $color-primary;color: $color-white;text-align: center;border-radius: $border-radius;cursor: pointer;transition: $transition-base;&:hover {background-color: $color-primary-hover;}}.file-input-hidden {display: none;}}// 文件列表容器.file-list-wrapper {flex: 1;overflow-y: auto;padding: 0.5rem;// 空文件状态.empty-file-state {@include flex-center;flex-direction: column;height: 100%;color: $color-gray-400;i {font-size: 3.5rem;margin-bottom: 1rem;}p {font-size: 1rem;margin-bottom: 0.25rem;}.empty-tip {font-size: 0.875rem;}}// 文件列表.file-list {list-style: none;display: flex;flex-direction: column;gap: 0.25rem;.file-item {@include flex-between;align-items: center;padding: 0.5rem;border-radius: $border-radius;cursor: pointer;transition: $transition-base;&:hover {background-color: $color-gray-100;}&.active {background-color: $color-primary-light;border-left: 4px solid $color-primary;}.file-info {@include flex-center;gap: 0.5rem;flex: 1;i {color: $color-orange;}.file-name {@include text-ellipsis;max-width: 160px;@media (min-width: 768px) {max-width: 200px;}}}.delete-file-btn {background: transparent;border: none;color: $color-gray-400;cursor: pointer;padding: 0.25rem;transition: $transition-base;&:hover {color: $color-red;}}}}}
}// 右侧预览区
.preview-main {flex: 1;display: flex;flex-direction: column;height: 100%;overflow: hidden;// 预览头部.preview-header {background-color: $color-gray-100;border-bottom: 1px solid $color-gray-200;padding: 0.75rem 1rem;@include flex-between;align-items: center;.preview-file-name {font-size: 1rem;font-weight: 500;@include text-ellipsis;max-width: 70%;}.view-mode-group {display: flex;gap: 0.5rem;.mode-btn {display: flex;align-items: center;gap: 0.25rem;padding: 0.25rem 0.75rem;font-size: 0.875rem;border-radius: $border-radius;background-color: $color-white;border: 1px solid $color-gray-200;box-shadow: $shadow-sm;cursor: pointer;transition: $transition-base;&:hover {background-color: $color-gray-100;}&.active {background-color: $color-primary;color: $color-white;border-color: $color-primary;}}}}// 预览内容区.preview-content {flex: 1;overflow: auto;padding: 1rem;background-color: $color-gray-50;// 空预览状态.empty-preview-state {@include flex-center;flex-direction: column;height: 100%;color: $color-gray-400;i {font-size: 3.5rem;margin-bottom: 1rem;}p {font-size: 1rem;}}// HTML预览模式.html-preview {background-color: $color-white;border-radius: $border-radius;box-shadow: $shadow-base;padding: 1.5rem;min-height: calc(100% - 2rem);.preview-content-inner {max-width: 100%;}}// 代码预览模式.code-preview {background-color: $color-dark;color: $color-light;border-radius: $border-radius;box-shadow: $shadow-base;padding: 1rem;min-height: calc(100% - 2rem);overflow: auto;.code-block {white-space: pre-wrap;word-break: break-all;font-family: monospace;font-size: 0.875rem;line-height: 1.5;}}}
}// 底部状态栏
.app-footer {background-color: $color-white;border-top: 1px solid $color-gray-200;padding: 0.5rem 1rem;font-size: 0.875rem;color: $color-gray-500;.footer-inner {@include flex-between;max-width: $container-max-width;margin: 0 auto;}.current-file-info,.file-count-info {display: flex;align-items: center;gap: 0.25rem;}
}
</style>
2、HTML预览
2.2: 实现代码
<script setup>
import { ref, watch, computed } from 'vue'// 状态管理
const files = ref([]) // 存储上传的HTML文件列表
const activeFileIndex = ref(-1) // 当前选中的文件索引
const viewMode = ref('preview') // 预览模式:'preview'(渲染预览)或 'code'(源代码)
const showHelp = ref(false) // 帮助提示框显示状态
const previewIframe = ref(null) // iframe元素引用// 计算属性:当前选中的文件
const activeFile = computed(() => {return activeFileIndex.value >= 0 ? files.value[activeFileIndex.value] : null
})// 监听选中文件或视图模式变化,更新iframe内容
watch(() => [activeFile.value?.content, viewMode.value],([content, mode]) => {if (mode === 'preview' && previewIframe.value && content) {// 获取iframe文档对象const iframeDoc = previewIframe.value.contentDocument// 写入完整HTML结构(包含基础样式重置)iframeDoc.open()iframeDoc.write(`<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>${activeFile.value.name} - 预览</title><style>/* 基础样式重置,避免继承父页面样式 */* {box-sizing: border-box;margin: 0;padding: 0;}body {font-family: -apple-system, BlinkMacSystemFont, sans-serif;line-height: 1.6;padding: 1rem;}</style></head><body>${content}</body></html>`)iframeDoc.close()}},{ immediate: true }
)// 处理文件上传
const handleFileUpload = (e) => {const file = e.target.files[0]if (!file) return// 验证文件类型(仅允许HTML)if (!file.name.endsWith('.html')) {alert('请上传扩展名为.html的文件')return}// 避免重复上传同名文件// const isDuplicate = files.value.some(item => item.name === file.name)// if (isDuplicate) {// alert(`文件 "${file.name}" 已存在,请选择其他文件或删除现有文件后重新上传`)// return// }// 读取文件内容(文本格式)const reader = new FileReader()reader.onload = (event) => {// 添加文件到列表files.value.push({name: file.name,content: event.target.result,size: formatFileSize(file.size)})// 自动选中新上传的文件activeFileIndex.value = files.value.length - 1}// 处理读取错误reader.onerror = () => {alert('文件读取失败,请重试或选择其他文件')}// 以文本形式读取文件reader.readAsText(file)// 重置文件输入框e.target.value = ''
}// 移除指定索引的文件
const removeFile = (index) => {const fileToDelete = files.value[index]if (confirm(`确定要删除文件 "${fileToDelete.name}" 吗?删除后无法恢复`)) {// 从列表中删除文件files.value.splice(index, 1)// 处理选中状态if (index === activeFileIndex.value) {activeFileIndex.value = files.value.length > 0 ? 0 : -1} else if (index < activeFileIndex.value) {activeFileIndex.value--}}
}// 工具函数:格式化文件大小(B → KB/MB)
const formatFileSize = (bytes) => {if (bytes < 1024) return `${bytes} B`if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
</script><template><div class="app-container"><!-- 顶部导航栏 --><header class="app-header"><div class="header-inner"><div class="logo-group"><i class="fa fa-html5"></i><h1>HTML文件浏览器</h1></div><div class="header-actions"><button class="help-btn" @click="showHelp = !showHelp"><i class="fa fa-question-circle"></i><span class="help-text">帮助</span></button></div></div></header><!-- 帮助提示框 --><div class="help-container" v-if="showHelp"><div class="help-inner"><div class="help-text-group"><h3>使用指南</h3><p>1. 点击"选择HTML文件"按钮选择文件</p><p>2. 在左侧文件列表中点击文件名查看内容</p><p>3. 可以删除已选择的文件</p></div><button class="close-help-btn" @click="showHelp = false"><i class="fa fa-times"></i></button></div></div><!-- 主内容区 --><main class="app-main"><!-- 左侧文件列表 --><div class="file-list-sidebar"><div class="upload-section"><label for="file-upload" class="upload-btn"><i class="fa fa-upload"></i>选择HTML文件</label><inputid="file-upload"type="file"accept=".html"@change="handleFileUpload"class="file-input-hidden"></div><div class="file-list-wrapper"><div class="empty-file-state" v-if="files.length === 0"><i class="fa fa-file-code-o"></i><p>没有选择的文件</p><p class="empty-tip">点击上方按钮选择HTML文件</p></div><ul class="file-list" v-else><liv-for="(file, index) in files":key="index"class="file-item":class="{ 'active': activeFileIndex === index }"@click="activeFileIndex = index"><div class="file-info"><i class="fa fa-file-html-o"></i><span class="file-name">{{ file.name }}</span></div><buttonclass="delete-file-btn"@click.stop="removeFile(index)"title="删除文件"><i class="fa fa-trash-o"></i></button></li></ul></div></div><!-- 右侧预览区 --><div class="preview-main"><div class="preview-header" v-if="activeFile"><h2 class="preview-file-name">{{ activeFile.name }}</h2><div class="view-mode-group"><buttonclass="mode-btn":class="{ 'active': viewMode === 'preview' }"@click="viewMode = 'preview'"><i class="fa fa-eye"></i>预览</button><buttonclass="mode-btn":class="{ 'active': viewMode === 'code' }"@click="viewMode = 'code'"><i class="fa fa-code"></i>代码</button></div></div><div class="preview-content"><div class="empty-preview-state" v-if="!activeFile"><i class="fa fa-eye"></i><p>请从左侧选择一个文件进行预览</p></div><!-- 预览模式 --><div class="html-preview" v-if="activeFile && viewMode === 'preview'"><iframeref="previewIframe"class="preview-content-inner"frameborder="0"title="HTML预览"></iframe></div><!-- 代码模式 --><div class="code-preview" v-if="activeFile && viewMode === 'code'"><pre class="code-block"><code>{{ activeFile.content }}</code></pre></div></div></div></main><!-- 底部状态栏 --><footer class="app-footer"><div class="footer-inner"><div class="current-file-info"><span v-if="activeFile"><i class="fa fa-file-text-o"></i>{{ activeFile.name }}</span><span v-else>未选择文件</span></div><div class="file-count-info"><span>{{ files.length }} 个文件</span></div></div></footer></div>
</template><style scoped lang="scss">
// 基础变量定义
$color-primary: #3b82f6;
$color-primary-light: rgba(59, 130, 246, 0.1);
$color-primary-hover: rgba(59, 130, 246, 0.9);
$color-orange: #f97316;
$color-red: #ef4444;
$color-gray-50: #f9fafb;
$color-gray-100: #f3f4f6;
$color-gray-200: #e5e7eb;
$color-gray-400: #9ca3af;
$color-gray-500: #6b7280;
$color-gray-600: #4b5563;
$color-gray-700: #374151;
$color-dark: #1e293b;
$color-light: #f1f5f9;
$color-white: #ffffff;$shadow-base: 0 4px 20px rgba(0, 0, 0, 0.08);
$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);$border-radius: 0.5rem;
$transition-base: all 0.2s ease;$container-max-width: 1280px;
$sidebar-width-mobile: 100%;
$sidebar-width-desktop: 20rem;// 工具混合宏
@mixin flex-center {display: flex;align-items: center;justify-content: center;
}@mixin flex-between {display: flex;align-items: center;justify-content: space-between;
}@mixin text-ellipsis {white-space: nowrap;overflow: hidden;text-overflow: ellipsis;
}// 基础样式重置
* {margin: 0;padding: 0;box-sizing: border-box;
}// 根容器样式
.app-container {display: flex;flex-direction: column;height: calc(100vh - 90px);overflow: hidden;background-color: $color-gray-50;font-family: 'Inter', system-ui, sans-serif;color: #1f2937;
}// 顶部导航栏样式
.app-header {background-color: $color-white;box-shadow: $shadow-sm;z-index: 10;padding: 0.75rem 1rem;.header-inner {@include flex-between;max-width: $container-max-width;margin: 0 auto;}.logo-group {@include flex-center;gap: 0.5rem;i {color: $color-orange;font-size: 1.5rem;}h1 {font-size: 1.25rem;font-weight: 600;color: $color-primary;}}.header-actions {.help-btn {@include flex-center;gap: 0.25rem;background: transparent;border: none;color: $color-gray-600;cursor: pointer;font-size: 1rem;transition: $transition-base;&:hover {color: $color-primary;}.help-text {display: none;@media (min-width: 768px) {display: inline;}}}}
}// 帮助提示框样式
.help-container {background-color: rgba(59, 130, 246, 0.05);border-left: 4px solid $color-primary;padding: 1rem;box-shadow: $shadow-sm;transition: $transition-base;.help-inner {@include flex-between;align-items: flex-start;max-width: $container-max-width;margin: 0 auto;}.help-text-group {h3 {font-size: 1rem;font-weight: 600;color: $color-primary;margin-bottom: 0.5rem;}p {font-size: 0.875rem;color: $color-gray-600;margin-bottom: 0.25rem;}}.close-help-btn {background: transparent;border: none;color: $color-gray-500;cursor: pointer;font-size: 1rem;transition: $transition-base;&:hover {color: $color-gray-700;}}
}// 主内容区样式
.app-main {display: flex;flex: 1;overflow: hidden;
}// 左侧文件列表侧边栏
.file-list-sidebar {width: $sidebar-width-mobile;background-color: $color-white;border-right: 1px solid $color-gray-200;display: flex;flex-direction: column;height: 100%;@media (min-width: 768px) {width: $sidebar-width-desktop;}// 上传区域.upload-section {padding: 1rem;border-bottom: 1px solid $color-gray-200;.upload-btn {@include flex-center;gap: 0.5rem;display: block;width: 100%;padding: 0.5rem 1rem;background-color: $color-primary;color: $color-white;text-align: center;border-radius: $border-radius;cursor: pointer;transition: $transition-base;&:hover {background-color: $color-primary-hover;}}.file-input-hidden {display: none;}}// 文件列表容器.file-list-wrapper {flex: 1;overflow-y: auto;padding: 0.5rem;// 空文件状态.empty-file-state {@include flex-center;flex-direction: column;height: 100%;color: $color-gray-400;i {font-size: 3.5rem;margin-bottom: 1rem;}p {font-size: 1rem;margin-bottom: 0.25rem;}.empty-tip {font-size: 0.875rem;}}// 文件列表.file-list {list-style: none;display: flex;flex-direction: column;gap: 0.25rem;.file-item {@include flex-between;align-items: center;padding: 0.5rem;border-radius: $border-radius;cursor: pointer;transition: $transition-base;&:hover {background-color: $color-gray-100;}&.active {background-color: $color-primary-light;border-left: 4px solid $color-primary;}.file-info {@include flex-center;gap: 0.5rem;flex: 1;i {color: $color-orange;}.file-name {@include text-ellipsis;max-width: 160px;@media (min-width: 768px) {max-width: 200px;}}}.delete-file-btn {background: transparent;border: none;color: $color-gray-400;cursor: pointer;padding: 0.25rem;transition: $transition-base;&:hover {color: $color-red;}}}}}
}// 右侧预览区
.preview-main {flex: 1;display: flex;flex-direction: column;height: 100%;overflow: hidden;// 预览头部.preview-header {background-color: $color-gray-100;border-bottom: 1px solid $color-gray-200;padding: 0.75rem 1rem;@include flex-between;align-items: center;.preview-file-name {font-size: 1rem;font-weight: 500;@include text-ellipsis;max-width: 70%;}.view-mode-group {display: flex;gap: 0.5rem;.mode-btn {display: flex;align-items: center;gap: 0.25rem;padding: 0.25rem 0.75rem;font-size: 0.875rem;border-radius: $border-radius;background-color: $color-white;border: 1px solid $color-gray-200;box-shadow: $shadow-sm;cursor: pointer;transition: $transition-base;&:hover {background-color: $color-gray-100;}&.active {background-color: $color-primary;color: $color-white;border-color: $color-primary;}}}}// 预览内容区.preview-content {flex: 1;overflow: auto;padding: 1rem;background-color: $color-gray-50;// 空预览状态.empty-preview-state {@include flex-center;flex-direction: column;height: 100%;color: $color-gray-400;i {font-size: 3.5rem;margin-bottom: 1rem;}p {font-size: 1rem;}}// HTML预览模式.html-preview {background-color: $color-white;border-radius: $border-radius;box-shadow: $shadow-base;padding: 1.5rem;min-height: calc(100% - 2rem);.preview-content-inner {width: 100%;height: calc(100vh - 270px);border-radius: 6px;box-shadow: 0 2px 10px rgba(0,0,0,0.05);background-color: white;}}// 代码预览模式.code-preview {background-color: $color-dark;color: $color-light;border-radius: $border-radius;box-shadow: $shadow-base;padding: 1rem;min-height: calc(100% - 2rem);overflow: auto;.code-block {white-space: pre-wrap;word-break: break-all;font-family: monospace;font-size: 0.875rem;line-height: 1.5;}}}
}// 底部状态栏
.app-footer {background-color: $color-white;border-top: 1px solid $color-gray-200;padding: 0.5rem 1rem;font-size: 0.875rem;color: $color-gray-500;.footer-inner {@include flex-between;max-width: $container-max-width;margin: 0 auto;}.current-file-info,.file-count-info {display: flex;align-items: center;gap: 0.25rem;}
}
</style>
3、MD文件预览
3.1 实现代码
<script setup>
import { ref, watch, computed } from 'vue'
import { marked } from 'marked' // 引入Markdown解析库
import hljs from 'highlight.js'
import 'highlight.js/styles/github-dark.css' // 引入代码高亮样式// 配置marked使用highlight.js进行代码高亮
marked.setOptions({highlight: function(code, lang) {// 如果指定了语言且hljs支持该语言if (lang && hljs.getLanguage(lang)) {return hljs.highlight(code, { language: lang }).value}// 未指定语言时尝试自动检测return hljs.highlightAuto(code).value},breaks: true, // 支持换行gfm: true // 支持GitHub Flavored Markdown
})// 状态管理
const files = ref([]) // 存储选择的文件列表
const activeFileIndex = ref(-1) // 当前选中的文件索引
const viewMode = ref('preview') // 预览模式:'preview'(渲染预览)或 'code'(源代码)
const showHelp = ref(false) // 帮助提示框显示状态// 计算属性:当前选中的文件
const activeFile = computed(() => {return activeFileIndex.value >= 0 ? files.value[activeFileIndex.value] : null
})// 计算属性:渲染后的Markdown内容
const renderedContent = computed(() => {if (activeFile.value && viewMode.value === 'preview') {return marked.parse(activeFile.value.content)}return ''
})// 监听选中文件索引变化,自动滚动到可视区域
watch(activeFileIndex, (newIndex) => {if (newIndex >= 0) {const fileItems = document.querySelectorAll('.file-item')if (fileItems[newIndex]) {fileItems[newIndex].scrollIntoView({ behavior: 'smooth', block: 'nearest' })}}
})// 处理文件选择
const handleFileUpload = (e) => {const file = e.target.files[0]if (!file) return// 验证文件类型(仅允许Markdown)const isMarkdown = file.name.endsWith('.md') || file.name.endsWith('.markdown')if (!isMarkdown) {alert('请选择扩展名为.md或.markdown的文件')return}// 避免重复选择同名文件const isDuplicate = files.value.some(item => item.name === file.name)if (isDuplicate) {alert(`文件 "${file.name}" 已存在,请选择其他文件或删除现有文件后重新选择`)return}// 读取文件内容(文本格式)const reader = new FileReader()reader.onload = (event) => {// 添加文件到列表files.value.push({name: file.name,content: event.target.result,size: formatFileSize(file.size)})// 自动选中新选择的文件activeFileIndex.value = files.value.length - 1}// 处理读取错误reader.onerror = () => {alert('文件读取失败,请重试或选择其他文件')}// 以文本形式读取文件reader.readAsText(file)// 重置文件输入框e.target.value = ''
}// 移除指定索引的文件
const removeFile = (index) => {const fileToDelete = files.value[index]if (confirm(`确定要删除文件 "${fileToDelete.name}" 吗?删除后无法恢复`)) {// 从列表中删除文件files.value.splice(index, 1)// 处理选中状态if (index === activeFileIndex.value) {activeFileIndex.value = files.value.length > 0 ? 0 : -1} else if (index < activeFileIndex.value) {activeFileIndex.value--}}
}// 工具函数:格式化文件大小(B → KB/MB)
const formatFileSize = (bytes) => {if (bytes < 1024) return `${bytes} B`if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
</script><template><div class="app-container"><!-- 顶部导航栏 --><header class="app-header"><div class="header-inner"><div class="logo-group"><i class="fa fa-markdown"></i><h1>Markdown文件浏览器</h1></div><div class="header-actions"><button class="help-btn" @click="showHelp = !showHelp"><i class="fa fa-question-circle"></i><span class="help-text">帮助</span></button></div></div></header><!-- 帮助提示框 --><div class="help-container" v-if="showHelp"><div class="help-inner"><div class="help-text-group"><h3>使用指南</h3><p>1. 点击"选择Markdown文件"按钮选择文件</p><p>2. 在左侧文件列表中点击文件名查看内容</p><p>3. 可以切换预览模式和代码模式</p><p>4. 可以删除已选择的文件</p></div><button class="close-help-btn" @click="showHelp = false"><i class="fa fa-times"></i></button></div></div><!-- 主内容区 --><main class="app-main"><!-- 左侧文件列表 --><div class="file-list-sidebar"><div class="upload-section"><label for="file-upload" class="upload-btn"><i class="fa fa-upload"></i>选择Markdown文件</label><inputid="file-upload"type="file"accept=".md,.markdown"@change="handleFileUpload"class="file-input-hidden"></div><div class="file-list-wrapper"><div class="empty-file-state" v-if="files.length === 0"><i class="fa fa-file-text-o"></i><p>没有选择的文件</p><p class="empty-tip">点击上方按钮选择Markdown文件</p></div><ul class="file-list" v-else><liv-for="(file, index) in files":key="index"class="file-item":class="{ 'active': activeFileIndex === index }"@click="activeFileIndex = index"><div class="file-info"><i class="fa fa-file-text-o"></i><span class="file-name">{{ file.name }}</span></div><buttonclass="delete-file-btn"@click.stop="removeFile(index)"title="删除文件"><i class="fa fa-trash-o"></i></button></li></ul></div></div><!-- 右侧预览区 --><div class="preview-main"><div class="preview-header" v-if="activeFile"><h2 class="preview-file-name">{{ activeFile.name }}</h2><div class="view-mode-group"><buttonclass="mode-btn":class="{ 'active': viewMode === 'preview' }"@click="viewMode = 'preview'"><i class="fa fa-eye"></i>预览</button><buttonclass="mode-btn":class="{ 'active': viewMode === 'code' }"@click="viewMode = 'code'"><i class="fa fa-code"></i>代码</button></div></div><div class="preview-content"><div class="empty-preview-state" v-if="!activeFile"><i class="fa fa-eye"></i><p>请从左侧选择一个文件进行预览</p></div><!-- 预览模式 --><div class="markdown-preview" v-if="activeFile && viewMode === 'preview'"><div class="preview-content-inner" v-html="renderedContent"></div></div><!-- 代码模式 --><div class="code-preview" v-if="activeFile && viewMode === 'code'"><pre class="code-block"><code>{{ activeFile.content }}</code></pre></div></div></div></main><!-- 底部状态栏 --><footer class="app-footer"><div class="footer-inner"><div class="current-file-info"><span v-if="activeFile"><i class="fa fa-file-text-o"></i>{{ activeFile.name }}</span><span v-else>未选择文件</span></div><div class="file-count-info"><span>{{ files.length }} 个文件</span></div></div></footer></div>
</template><style scoped lang="scss">
// 基础变量定义
$color-primary: #3b82f6;
$color-primary-light: rgba(59, 130, 246, 0.1);
$color-primary-hover: rgba(59, 130, 246, 0.9);
$color-purple: #9333ea; /* Markdown主色调 */
$color-red: #ef4444;
$color-gray-50: #f9fafb;
$color-gray-100: #f3f4f6;
$color-gray-200: #e5e7eb;
$color-gray-400: #9ca3af;
$color-gray-500: #6b7280;
$color-gray-600: #4b5563;
$color-gray-700: #374151;
$color-dark: #1e293b;
$color-light: #f1f5f9;
$color-white: #ffffff;$shadow-base: 0 4px 20px rgba(0, 0, 0, 0.08);
$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);$border-radius: 0.5rem;
$transition-base: all 0.2s ease;$container-max-width: 1280px;
$sidebar-width-mobile: 100%;
$sidebar-width-desktop: 20rem;// Markdown预览样式变量
$markdown-font-size: 1rem;
$markdown-line-height: 1.6;
$markdown-max-width: 800px;// 工具混合宏
@mixin flex-center {display: flex;align-items: center;justify-content: center;
}@mixin flex-between {display: flex;align-items: center;justify-content: space-between;
}@mixin text-ellipsis {white-space: nowrap;overflow: hidden;text-overflow: ellipsis;
}// 基础样式重置
* {margin: 0;padding: 0;box-sizing: border-box;
}// 根容器样式
.app-container {display: flex;flex-direction: column;height: calc(100vh - 90px);overflow: hidden;background-color: $color-gray-50;font-family: 'Inter', system-ui, sans-serif;color: #1f2937;
}// 顶部导航栏样式
.app-header {background-color: $color-white;box-shadow: $shadow-sm;z-index: 10;padding: 0.75rem 1rem;.header-inner {@include flex-between;max-width: $container-max-width;margin: 0 auto;}.logo-group {@include flex-center;gap: 0.5rem;i {color: $color-purple;font-size: 1.5rem;}h1 {font-size: 1.25rem;font-weight: 600;color: $color-primary;}}.header-actions {.help-btn {@include flex-center;gap: 0.25rem;background: transparent;border: none;color: $color-gray-600;cursor: pointer;font-size: 1rem;transition: $transition-base;&:hover {color: $color-primary;}.help-text {display: none;@media (min-width: 768px) {display: inline;}}}}
}// 帮助提示框样式
.help-container {background-color: rgba(59, 130, 246, 0.05);border-left: 4px solid $color-primary;padding: 1rem;box-shadow: $shadow-sm;transition: $transition-base;.help-inner {@include flex-between;align-items: flex-start;max-width: $container-max-width;margin: 0 auto;}.help-text-group {h3 {font-size: 1rem;font-weight: 600;color: $color-primary;margin-bottom: 0.5rem;}p {font-size: 0.875rem;color: $color-gray-600;margin-bottom: 0.25rem;}}.close-help-btn {background: transparent;border: none;color: $color-gray-500;cursor: pointer;font-size: 1rem;transition: $transition-base;&:hover {color: $color-gray-700;}}
}// 主内容区样式
.app-main {display: flex;flex: 1;overflow: hidden;
}// 左侧文件列表侧边栏
.file-list-sidebar {width: $sidebar-width-mobile;background-color: $color-white;border-right: 1px solid $color-gray-200;display: flex;flex-direction: column;height: 100%;@media (min-width: 768px) {width: $sidebar-width-desktop;}// 选择区域.upload-section {padding: 1rem;border-bottom: 1px solid $color-gray-200;.upload-btn {@include flex-center;gap: 0.5rem;display: block;width: 100%;padding: 0.5rem 1rem;background-color: $color-primary;color: $color-white;text-align: center;border-radius: $border-radius;cursor: pointer;transition: $transition-base;&:hover {background-color: $color-primary-hover;}}.file-input-hidden {display: none;}}// 文件列表容器.file-list-wrapper {flex: 1;overflow-y: auto;padding: 0.5rem;// 空文件状态.empty-file-state {@include flex-center;flex-direction: column;height: 100%;color: $color-gray-400;i {font-size: 3.5rem;margin-bottom: 1rem;}p {font-size: 1rem;margin-bottom: 0.25rem;}.empty-tip {font-size: 0.875rem;}}// 文件列表.file-list {list-style: none;display: flex;flex-direction: column;gap: 0.25rem;.file-item {@include flex-between;align-items: center;padding: 0.5rem;border-radius: $border-radius;cursor: pointer;transition: $transition-base;&:hover {background-color: $color-gray-100;}&.active {background-color: $color-primary-light;border-left: 4px solid $color-primary;}.file-info {@include flex-center;gap: 0.5rem;flex: 1;i {color: $color-purple;}.file-name {@include text-ellipsis;max-width: 160px;@media (min-width: 768px) {max-width: 200px;}}}.delete-file-btn {background: transparent;border: none;color: $color-gray-400;cursor: pointer;padding: 0.25rem;transition: $transition-base;&:hover {color: $color-red;}}}}}
}// 右侧预览区
.preview-main {flex: 1;display: flex;flex-direction: column;height: 100%;overflow: hidden;// 预览头部.preview-header {background-color: $color-gray-100;border-bottom: 1px solid $color-gray-200;padding: 0.75rem 1rem;@include flex-between;align-items: center;.preview-file-name {font-size: 1rem;font-weight: 500;@include text-ellipsis;max-width: 70%;}.view-mode-group {display: flex;gap: 0.5rem;.mode-btn {display: flex;align-items: center;gap: 0.25rem;padding: 0.25rem 0.75rem;font-size: 0.875rem;border-radius: $border-radius;background-color: $color-white;border: 1px solid $color-gray-200;box-shadow: $shadow-sm;cursor: pointer;transition: $transition-base;&:hover {background-color: $color-gray-100;}&.active {background-color: $color-primary;color: $color-white;border-color: $color-primary;}}}}// 预览内容区.preview-content {flex: 1;overflow: auto;padding: 1rem;background-color: $color-gray-50;// 空预览状态.empty-preview-state {@include flex-center;flex-direction: column;height: 100%;color: $color-gray-400;i {font-size: 3.5rem;margin-bottom: 1rem;}p {font-size: 1rem;}}// Markdown预览模式.markdown-preview {background-color: $color-white;border-radius: $border-radius;box-shadow: $shadow-base;padding: 2rem;min-height: calc(100% - 2rem);.preview-content-inner {max-width: $markdown-max-width;margin: 0 auto;font-size: $markdown-font-size;line-height: $markdown-line-height;// Markdown基础样式h1, h2, h3, h4, h5, h6 {margin-top: 1.5em;margin-bottom: 0.5em;font-weight: 600;color: $color-dark;}h1 {font-size: 1.8rem;border-bottom: 1px solid $color-gray-200;padding-bottom: 0.5rem;}h2 {font-size: 1.5rem;border-bottom: 1px solid $color-gray-200;padding-bottom: 0.5rem;}p {margin-bottom: 1em;}ul, ol {margin-left: 1.5rem;margin-bottom: 1em;}ul {list-style-type: disc;}ol {list-style-type: decimal;}li {margin-bottom: 0.5em;}a {color: $color-primary;text-decoration: none;&:hover {text-decoration: underline;}}code {background-color: $color-gray-100;padding: 0.2em 0.4em;border-radius: 0.25rem;font-family: monospace;font-size: 0.9em;}pre {background-color: $color-dark;color: $color-light;padding: 1rem;border-radius: $border-radius;overflow-x: auto;margin-bottom: 1em;font-family: monospace;}pre code {background-color: transparent;padding: 0;font-size: 0.9em;}blockquote {border-left: 4px solid $color-gray-200;padding-left: 1rem;margin-left: 0;margin-bottom: 1em;color: $color-gray-600;}img {max-width: 100%;height: auto;margin: 1em 0;border-radius: $border-radius;}table {border-collapse: collapse;width: 100%;margin-bottom: 1em;}th, td {border: 1px solid $color-gray-200;padding: 0.5rem 1rem;text-align: left;}th {background-color: $color-gray-50;}}}// 代码模式.code-preview {background-color: $color-dark;color: $color-light;border-radius: $border-radius;box-shadow: $shadow-base;padding: 1rem;min-height: calc(100% - 2rem);overflow: auto;.code-block {white-space: pre-wrap;word-break: break-all;font-family: monospace;font-size: 0.875rem;line-height: 1.5;}}}
}// 底部状态栏
.app-footer {background-color: $color-white;border-top: 1px solid $color-gray-200;padding: 0.5rem 1rem;font-size: 0.875rem;color: $color-gray-500;.footer-inner {@include flex-between;max-width: $container-max-width;margin: 0 auto;}.current-file-info,.file-count-info {display: flex;align-items: center;gap: 0.25rem;}
}
</style>