PDF,HTML,md格式文件在线查看工具

发布于:2025-08-30 ⋅ 阅读:(22) ⋅ 点赞:(0)

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) return
  for (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
    // }
    
    // 生成本地 URL
    const 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>
          <input
            id="file-upload"
            type="file"
            multiple
            accept=".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>
            <li
              v-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>
              <button
                class="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>
          <input
            id="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>
            <li
              v-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>
              <button
                class="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">
            <button
              class="mode-btn"
              :class="{ 'active': viewMode === 'preview' }"
              @click="viewMode = 'preview'"
            >
              <i class="fa fa-eye"></i>预览
            </button>
            <button
              class="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'">
            <iframe
              ref="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>
          <input
            id="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>
            <li
              v-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>
              <button
                class="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">
            <button
              class="mode-btn"
              :class="{ 'active': viewMode === 'preview' }"
              @click="viewMode = 'preview'"
            >
              <i class="fa fa-eye"></i>预览
            </button>
            <button
              class="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>