Skip to content

组件文档

使用 VitePress 构建专业的组件文档网站,为 UI 组件库提供完整的文档、示例和交互式演示。

项目概述

组件文档是 UI 组件库的重要组成部分,需要清晰地展示组件的用法、属性、事件等信息。VitePress 凭借其强大的 Markdown 扩展能力和 Vue 组件集成,是构建组件文档的理想选择。

核心特性

  • 📚 完整文档 - 详细的组件说明和使用指南
  • 🎮 实时预览 - 在线编辑和预览组件效果
  • 💻 代码示例 - 丰富的使用示例和最佳实践
  • 🎨 主题定制 - 支持多种主题和样式定制
  • 📱 响应式 - 完美适配各种设备尺寸
  • 🔍 快速搜索 - 强大的组件和属性搜索功能
  • 📋 属性表格 - 清晰的属性、事件、插槽说明
  • 🚀 性能优化 - 按需加载和代码分割

技术架构

核心技术栈

json
{
  "framework": "VitePress",
  "language": "TypeScript",
  "styling": "CSS3 + PostCSS",
  "components": "Vue 3",
  "tools": [
    "Monaco Editor",
    "Prism.js",
    "Vue SFC Playground",
    "Vite"
  ]
}

项目结构

component-docs/
├── docs/
│   ├── .vitepress/
│   │   ├── config.ts
│   │   ├── theme/
│   │   │   ├── index.ts
│   │   │   ├── Layout.vue
│   │   │   └── components/
│   │   │       ├── Demo.vue
│   │   │       ├── ApiTable.vue
│   │   │       ├── CodeEditor.vue
│   │   │       └── ComponentPreview.vue
│   │   └── components/
│   │       └── ui/
│   ├── components/
│   │   ├── button/
│   │   ├── input/
│   │   ├── modal/
│   │   └── table/
│   ├── guide/
│   └── examples/
├── src/
│   └── components/
└── package.json

实现步骤

1. 项目初始化

bash
# 创建项目
npm create vitepress@latest component-docs
cd component-docs

# 安装依赖
npm install
npm install -D @vue/compiler-sfc monaco-editor @monaco-editor/loader

2. 配置 VitePress

typescript
// .vitepress/config.ts
import { defineConfig } from 'vitepress'

export default defineConfig({
  title: 'UI 组件库',
  description: '现代化的 Vue 组件库文档',
  
  head: [
    ['link', { rel: 'icon', href: '/favicon.ico' }],
    ['meta', { name: 'theme-color', content: '#646cff' }]
  ],
  
  themeConfig: {
    logo: '/logo.svg',
    
    nav: [
      { text: '指南', link: '/guide/' },
      { text: '组件', link: '/components/' },
      { text: '示例', link: '/examples/' },
      { text: 'GitHub', link: 'https://github.com/your-org/ui-components' }
    ],
    
    sidebar: {
      '/guide/': [
        {
          text: '开始使用',
          items: [
            { text: '快速开始', link: '/guide/' },
            { text: '安装', link: '/guide/installation' },
            { text: '主题定制', link: '/guide/theming' }
          ]
        }
      ],
      '/components/': [
        {
          text: '基础组件',
          items: [
            { text: 'Button 按钮', link: '/components/button' },
            { text: 'Input 输入框', link: '/components/input' },
            { text: 'Icon 图标', link: '/components/icon' }
          ]
        },
        {
          text: '布局组件',
          items: [
            { text: 'Grid 栅格', link: '/components/grid' },
            { text: 'Layout 布局', link: '/components/layout' },
            { text: 'Space 间距', link: '/components/space' }
          ]
        },
        {
          text: '反馈组件',
          items: [
            { text: 'Modal 对话框', link: '/components/modal' },
            { text: 'Message 消息', link: '/components/message' },
            { text: 'Loading 加载', link: '/components/loading' }
          ]
        }
      ]
    },
    
    socialLinks: [
      { icon: 'github', link: 'https://github.com/your-org/ui-components' }
    ]
  },
  
  markdown: {
    config: (md) => {
      // 自定义 markdown 插件
      md.use(require('./plugins/demo-block'))
    }
  },
  
  vite: {
    resolve: {
      alias: {
        '@': '/src'
      }
    }
  }
})

3. 组件演示容器

vue
<!-- .vitepress/theme/components/Demo.vue -->
<template>
  <div class="demo-block">
    <div class="demo-preview" :class="{ 'demo-dark': isDark }">
      <component :is="DemoComponent" v-if="DemoComponent" />
      <div v-else class="demo-error">
        组件加载失败
      </div>
    </div>
    
    <div class="demo-actions">
      <button @click="toggleCode" class="action-btn">
        <Icon name="code" />
        {{ showCode ? '隐藏代码' : '查看代码' }}
      </button>
      
      <button @click="copyCode" class="action-btn">
        <Icon name="copy" />
        复制代码
      </button>
      
      <button @click="openPlayground" class="action-btn">
        <Icon name="external" />
        在线编辑
      </button>
      
      <button @click="toggleTheme" class="action-btn">
        <Icon :name="isDark ? 'sun' : 'moon'" />
        {{ isDark ? '浅色' : '深色' }}
      </button>
    </div>
    
    <div v-show="showCode" class="demo-code">
      <div class="code-header">
        <span class="code-lang">vue</span>
        <button @click="copyCode" class="copy-btn">
          <Icon name="copy" />
        </button>
      </div>
      <pre><code v-html="highlightedCode"></code></pre>
    </div>
    
    <div v-if="description" class="demo-description">
      <div v-html="description"></div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import { useData } from 'vitepress'
import Prism from 'prismjs'
import 'prismjs/components/prism-typescript'
import 'prismjs/components/prism-javascript'
import Icon from './Icon.vue'

const props = defineProps({
  source: String,
  path: String,
  rawSource: String,
  description: String
})

const { isDark } = useData()
const showCode = ref(false)
const DemoComponent = ref(null)

const highlightedCode = computed(() => {
  if (!props.rawSource) return ''
  return Prism.highlight(props.rawSource, Prism.languages.javascript, 'javascript')
})

onMounted(async () => {
  if (props.path) {
    try {
      const module = await import(/* @vite-ignore */ props.path)
      DemoComponent.value = module.default
    } catch (error) {
      console.error('Failed to load demo component:', error)
    }
  }
})

function toggleCode() {
  showCode.value = !showCode.value
}

function copyCode() {
  if (props.rawSource) {
    navigator.clipboard.writeText(props.rawSource)
    // 显示复制成功提示
  }
}

function openPlayground() {
  const playgroundUrl = `https://sfc.vuejs.org/#${btoa(props.rawSource)}`
  window.open(playgroundUrl, '_blank')
}

function toggleTheme() {
  // 切换演示区域主题
}
</script>

<style scoped>
.demo-block {
  border: 1px solid var(--vp-c-border);
  border-radius: 8px;
  margin: 20px 0;
  overflow: hidden;
}

.demo-preview {
  padding: 24px;
  background: var(--vp-c-bg);
  border-bottom: 1px solid var(--vp-c-border);
  position: relative;
}

.demo-preview.demo-dark {
  background: #1a1a1a;
  color: #fff;
}

.demo-error {
  color: var(--vp-c-danger);
  text-align: center;
  padding: 20px;
}

.demo-actions {
  display: flex;
  gap: 8px;
  padding: 12px 16px;
  background: var(--vp-c-bg-mute);
  border-bottom: 1px solid var(--vp-c-border);
}

.action-btn {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 6px 12px;
  background: transparent;
  border: 1px solid var(--vp-c-border);
  border-radius: 4px;
  color: var(--vp-c-text-2);
  cursor: pointer;
  transition: all 0.2s;
  font-size: 12px;
}

.action-btn:hover {
  background: var(--vp-c-bg);
  color: var(--vp-c-text-1);
}

.demo-code {
  position: relative;
}

.code-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 16px;
  background: var(--vp-c-bg-mute);
  border-bottom: 1px solid var(--vp-c-border);
}

.code-lang {
  font-size: 12px;
  color: var(--vp-c-text-2);
  font-weight: 500;
}

.copy-btn {
  padding: 4px;
  background: transparent;
  border: none;
  color: var(--vp-c-text-2);
  cursor: pointer;
  border-radius: 4px;
  transition: all 0.2s;
}

.copy-btn:hover {
  background: var(--vp-c-bg);
  color: var(--vp-c-text-1);
}

.demo-code pre {
  margin: 0;
  padding: 16px;
  background: var(--vp-code-block-bg);
  overflow-x: auto;
}

.demo-code code {
  font-family: var(--vp-font-family-mono);
  font-size: 14px;
  line-height: 1.5;
}

.demo-description {
  padding: 16px;
  background: var(--vp-c-bg-soft);
  color: var(--vp-c-text-2);
  font-size: 14px;
  line-height: 1.6;
}
</style>

4. API 属性表格

vue
<!-- .vitepress/theme/components/ApiTable.vue -->
<template>
  <div class="api-table">
    <h3 v-if="title">{{ title }}</h3>
    
    <div class="table-container">
      <table>
        <thead>
          <tr>
            <th v-for="column in columns" :key="column.key">
              {{ column.title }}
            </th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="(row, index) in data" :key="index">
            <td v-for="column in columns" :key="column.key">
              <template v-if="column.key === 'name'">
                <code class="prop-name">{{ row[column.key] }}</code>
              </template>
              <template v-else-if="column.key === 'type'">
                <code class="prop-type">{{ row[column.key] }}</code>
              </template>
              <template v-else-if="column.key === 'default'">
                <code v-if="row[column.key]" class="prop-default">
                  {{ row[column.key] }}
                </code>
                <span v-else class="prop-empty">-</span>
              </template>
              <template v-else-if="column.key === 'required'">
                <span :class="['prop-required', { 'is-required': row[column.key] }]">
                  {{ row[column.key] ? '是' : '否' }}
                </span>
              </template>
              <template v-else>
                {{ row[column.key] }}
              </template>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</template>

<script setup>
defineProps({
  title: String,
  columns: {
    type: Array,
    default: () => [
      { key: 'name', title: '属性名' },
      { key: 'description', title: '说明' },
      { key: 'type', title: '类型' },
      { key: 'default', title: '默认值' },
      { key: 'required', title: '必填' }
    ]
  },
  data: {
    type: Array,
    default: () => []
  }
})
</script>

<style scoped>
.api-table {
  margin: 24px 0;
}

.api-table h3 {
  margin-bottom: 16px;
  font-size: 18px;
  font-weight: 600;
  color: var(--vp-c-text-1);
}

.table-container {
  overflow-x: auto;
  border: 1px solid var(--vp-c-border);
  border-radius: 8px;
}

table {
  width: 100%;
  border-collapse: collapse;
  background: var(--vp-c-bg);
}

th {
  background: var(--vp-c-bg-mute);
  padding: 12px 16px;
  text-align: left;
  font-weight: 600;
  color: var(--vp-c-text-1);
  border-bottom: 1px solid var(--vp-c-border);
  white-space: nowrap;
}

td {
  padding: 12px 16px;
  border-bottom: 1px solid var(--vp-c-border);
  color: var(--vp-c-text-2);
  vertical-align: top;
}

tr:last-child td {
  border-bottom: none;
}

.prop-name {
  color: var(--vp-c-brand);
  background: var(--vp-code-bg);
  padding: 2px 6px;
  border-radius: 4px;
  font-size: 13px;
  font-weight: 500;
}

.prop-type {
  color: var(--vp-c-purple);
  background: var(--vp-code-bg);
  padding: 2px 6px;
  border-radius: 4px;
  font-size: 13px;
}

.prop-default {
  color: var(--vp-c-green);
  background: var(--vp-code-bg);
  padding: 2px 6px;
  border-radius: 4px;
  font-size: 13px;
}

.prop-empty {
  color: var(--vp-c-text-3);
  font-style: italic;
}

.prop-required {
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: 500;
}

.prop-required.is-required {
  background: var(--vp-c-danger-soft);
  color: var(--vp-c-danger);
}

.prop-required:not(.is-required) {
  background: var(--vp-c-bg-mute);
  color: var(--vp-c-text-3);
}

@media (max-width: 768px) {
  th, td {
    padding: 8px 12px;
    font-size: 14px;
  }
  
  .prop-name,
  .prop-type,
  .prop-default {
    font-size: 12px;
  }
}
</style>

5. 代码编辑器

vue
<!-- .vitepress/theme/components/CodeEditor.vue -->
<template>
  <div class="code-editor">
    <div class="editor-header">
      <div class="editor-tabs">
        <button
          v-for="file in files"
          :key="file.name"
          :class="['tab', { active: activeFile === file.name }]"
          @click="setActiveFile(file.name)"
        >
          {{ file.name }}
        </button>
      </div>
      
      <div class="editor-actions">
        <button @click="runCode" class="run-btn">
          <Icon name="play" />
          运行
        </button>
        <button @click="resetCode" class="reset-btn">
          <Icon name="refresh" />
          重置
        </button>
      </div>
    </div>
    
    <div class="editor-content">
      <div class="editor-pane">
        <div ref="editorRef" class="monaco-editor"></div>
      </div>
      
      <div class="preview-pane">
        <iframe
          ref="previewRef"
          class="preview-frame"
          sandbox="allow-scripts allow-same-origin"
        ></iframe>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import * as monaco from 'monaco-editor'
import { compileTemplate, compileScript } from '@vue/compiler-sfc'
import Icon from './Icon.vue'

const props = defineProps({
  files: {
    type: Array,
    default: () => [
      {
        name: 'App.vue',
        content: `<template>
  <div>
    <h1>Hello World</h1>
  </div>
</template>

<script setup>
// Your code here
</script>

<style scoped>
h1 {
  color: #42b883;
}
</style>`
      }
    ]
  }
})

const editorRef = ref()
const previewRef = ref()
const activeFile = ref('App.vue')

let editor = null
let currentFiles = ref([...props.files])

onMounted(() => {
  initMonaco()
  runCode()
})

onUnmounted(() => {
  if (editor) {
    editor.dispose()
  }
})

watch(activeFile, (newFile) => {
  if (editor) {
    const file = currentFiles.value.find(f => f.name === newFile)
    if (file) {
      editor.setValue(file.content)
    }
  }
})

function initMonaco() {
  editor = monaco.editor.create(editorRef.value, {
    value: currentFiles.value[0].content,
    language: 'vue',
    theme: 'vs-dark',
    fontSize: 14,
    minimap: { enabled: false },
    scrollBeyondLastLine: false,
    automaticLayout: true
  })
  
  editor.onDidChangeModelContent(() => {
    const file = currentFiles.value.find(f => f.name === activeFile.value)
    if (file) {
      file.content = editor.getValue()
    }
  })
}

function setActiveFile(fileName) {
  // 保存当前文件内容
  const currentFile = currentFiles.value.find(f => f.name === activeFile.value)
  if (currentFile && editor) {
    currentFile.content = editor.getValue()
  }
  
  activeFile.value = fileName
}

async function runCode() {
  try {
    const appFile = currentFiles.value.find(f => f.name === 'App.vue')
    if (!appFile) return
    
    const compiledCode = await compileVueComponent(appFile.content)
    const html = generatePreviewHTML(compiledCode)
    
    const blob = new Blob([html], { type: 'text/html' })
    const url = URL.createObjectURL(blob)
    
    previewRef.value.src = url
    
    // 清理旧的 URL
    setTimeout(() => URL.revokeObjectURL(url), 1000)
  } catch (error) {
    console.error('Compilation error:', error)
    showError(error.message)
  }
}

async function compileVueComponent(source) {
  // 简化的 Vue SFC 编译
  const { descriptor } = await import('@vue/compiler-sfc').then(m => 
    m.parse(source, { filename: 'App.vue' })
  )
  
  let compiledScript = ''
  let compiledTemplate = ''
  let compiledStyle = ''
  
  if (descriptor.script || descriptor.scriptSetup) {
    const script = descriptor.script || descriptor.scriptSetup
    compiledScript = script.content
  }
  
  if (descriptor.template) {
    const { code } = compileTemplate({
      source: descriptor.template.content,
      filename: 'App.vue',
      id: 'app'
    })
    compiledTemplate = code
  }
  
  if (descriptor.styles.length > 0) {
    compiledStyle = descriptor.styles.map(style => style.content).join('\n')
  }
  
  return {
    script: compiledScript,
    template: compiledTemplate,
    style: compiledStyle
  }
}

function generatePreviewHTML(compiled) {
  return `
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  <style>
    body { margin: 0; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
    ${compiled.style}
  </style>
</head>
<body>
  <div id="app"></div>
  <script>
    const { createApp } = Vue;
    
    const App = {
      ${compiled.script}
      template: \`${compiled.template}\`
    };
    
    createApp(App).mount('#app');
  </script>
</body>
</html>`
}

function resetCode() {
  const originalFile = props.files.find(f => f.name === activeFile.value)
  if (originalFile && editor) {
    editor.setValue(originalFile.content)
    
    // 重置当前文件内容
    const currentFile = currentFiles.value.find(f => f.name === activeFile.value)
    if (currentFile) {
      currentFile.content = originalFile.content
    }
  }
}

function showError(message) {
  const errorHTML = `
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    body { margin: 0; padding: 20px; font-family: monospace; }
    .error { color: #e74c3c; background: #fdf2f2; padding: 15px; border-radius: 4px; }
  </style>
</head>
<body>
  <div class="error">
    <h3>编译错误</h3>
    <pre>${message}</pre>
  </div>
</body>
</html>`
  
  const blob = new Blob([errorHTML], { type: 'text/html' })
  const url = URL.createObjectURL(blob)
  previewRef.value.src = url
  setTimeout(() => URL.revokeObjectURL(url), 1000)
}
</script>

<style scoped>
.code-editor {
  border: 1px solid var(--vp-c-border);
  border-radius: 8px;
  overflow: hidden;
  margin: 20px 0;
}

.editor-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  background: var(--vp-c-bg-mute);
  border-bottom: 1px solid var(--vp-c-border);
  padding: 8px 16px;
}

.editor-tabs {
  display: flex;
  gap: 4px;
}

.tab {
  padding: 6px 12px;
  background: transparent;
  border: none;
  border-radius: 4px;
  color: var(--vp-c-text-2);
  cursor: pointer;
  transition: all 0.2s;
  font-size: 13px;
}

.tab.active {
  background: var(--vp-c-brand);
  color: white;
}

.tab:hover:not(.active) {
  background: var(--vp-c-bg);
  color: var(--vp-c-text-1);
}

.editor-actions {
  display: flex;
  gap: 8px;
}

.run-btn,
.reset-btn {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 6px 12px;
  background: var(--vp-c-brand);
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
  transition: all 0.2s;
}

.reset-btn {
  background: var(--vp-c-text-3);
}

.run-btn:hover {
  background: var(--vp-c-brand-dark);
}

.reset-btn:hover {
  background: var(--vp-c-text-2);
}

.editor-content {
  display: grid;
  grid-template-columns: 1fr 1fr;
  height: 500px;
}

.editor-pane {
  border-right: 1px solid var(--vp-c-border);
}

.monaco-editor {
  height: 100%;
}

.preview-pane {
  background: white;
}

.preview-frame {
  width: 100%;
  height: 100%;
  border: none;
}

@media (max-width: 768px) {
  .editor-content {
    grid-template-columns: 1fr;
    grid-template-rows: 1fr 1fr;
    height: 600px;
  }
  
  .editor-pane {
    border-right: none;
    border-bottom: 1px solid var(--vp-c-border);
  }
}
</style>

6. 组件预览

vue
<!-- .vitepress/theme/components/ComponentPreview.vue -->
<template>
  <div class="component-preview">
    <div class="preview-header">
      <h3>{{ title }}</h3>
      <div class="preview-controls">
        <select v-model="selectedVariant" @change="updatePreview">
          <option v-for="variant in variants" :key="variant.name" :value="variant.name">
            {{ variant.label }}
          </option>
        </select>
        
        <button @click="toggleFullscreen" class="fullscreen-btn">
          <Icon :name="isFullscreen ? 'minimize' : 'maximize'" />
        </button>
      </div>
    </div>
    
    <div class="preview-content" :class="{ fullscreen: isFullscreen }">
      <div class="preview-viewport" :style="viewportStyle">
        <component
          :is="component"
          v-bind="currentProps"
          @update:modelValue="handleUpdate"
        />
      </div>
      
      <div class="preview-props" v-if="showProps">
        <h4>属性配置</h4>
        <div class="prop-controls">
          <div
            v-for="(prop, key) in editableProps"
            :key="key"
            class="prop-control"
          >
            <label>{{ prop.label || key }}</label>
            <component
              :is="getControlComponent
# Component Docs

本文档正在建设中,敬请期待。

## 概述

这里将提供关于 Component Docs 的详细信息和指导。

## 主要内容

- 基础概念介绍
- 使用方法说明
- 最佳实践建议
- 常见问题解答

## 相关资源

- [VitePress 官方文档](https://vitepress.dev/)
- [Vue.js 官方文档](https://vuejs.org/)
- [更多教程](../tutorials/index)

---

*本文档将持续更新,如有问题请通过 [GitHub Issues](https://github.com/shingle666) 反馈。*

vitepress开发指南