组件文档
使用 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) 反馈。*