在 docs/examples/button
目录下新增文件 basis.vue
1<template>
2 <div>
3 <suit-button>默认</suit-button>
4 <suit-button type="primary">主要</suit-button>
5 <suit-button type="success">成功</suit-button>
6 <suit-button type="warning">警告</suit-button>
7 <suit-button type="error">错误</suit-button>
8 </div>
9</template>
自定义容器
docs/.vitepress/config.mts
1import path from "node:path";
2import fs from "node:fs";
3import { defineConfig } from "vitepress";
4import MdContainer from "markdown-it-container";
5
6export default defineConfig({
7 markdown: {
8 // md 是 markdown-it 的实例
9 config: (md) => {
10 // markdown-it的use方法用于安装插件
11 md.use(MdContainer, "demo", {
12 render(tokens, idx) {
13 // 拿到tokens中的内容
14 if (tokens[idx].nesting === 1) {
15 // 获取:::demo后跟随的描述文本
16 const info = tokens[idx].info.trim().match(/^demo\s*(.*)$/);
17 const description = info && info.length > 1 ? info[1] : "";
18
19 // 获取文档组件路径
20 const nextToken = tokens[idx + 1];
21 const componentPath =
22 nextToken.type === "fence" ? nextToken.content : "";
23 return `<demo>`;
24 } else {
25 return `</demo>`;
26 }
27 },
28 });
29 },
30 },
31});
1import path from "node:path";
2import fs from "node:fs";
3import { defineConfig } from "vitepress";
4import MdContainer from "markdown-it-container";
5
6export default defineConfig({
7 markdown: {
8 // md 是 markdown-it 的实例
9 config: (md) => {
10 // markdown-it的use方法用于安装插件
11 md.use(MdContainer, "demo", {
12 render(tokens, idx) {
13 // 拿到tokens中的内容
14 if (tokens[idx].nesting === 1) {
15 // 获取:::demo后跟随的描述文本
16 const info = tokens[idx].info.trim().match(/^demo\s*(.*)$/);
17 const description = info && info.length > 1 ? info[1] : "";
18
19 // 获取文档组件路径
20 const nextToken = tokens[idx + 1];
21 const componentPath =
22 nextToken.type === "fence" ? nextToken.content : "";
23
24 let source = "";
25 if (componentPath) {
26 // /better-ui/docs/examples/button/basis.vue
27 let file = path.resolve(
28 __dirname,
29 "../examples",
30 `${componentPath}.vue`
31 );
32 file = file.replace(/\s+/g, "");
33 source = fs.readFileSync(file, "utf-8");
34 console.log(source);
35 }
36 return `<demo>`;
37 } else {
38 return `</demo>`;
39 }
40 },
41 });
42 },
43 },
44});
插槽渲染描述和源码
1import path from "node:path";
2import fs from "node:fs";
3import { defineConfig } from "vitepress";
4import MdContainer from "markdown-it-container";
5
6export default defineConfig({
7 markdown: {
8 // md 是 markdown-it 的实例
9 config: (md) => {
10 // markdown-it的use方法用于安装插件
11 md.use(MdContainer, "demo", {
12 render(tokens, idx) {
13 // 拿到tokens中的内容
14 if (tokens[idx].nesting === 1) {
15 // 获取:::demo后跟随的描述文本
16 const info = tokens[idx].info.trim().match(/^demo\s*(.*)$/);
17 const description = info && info.length > 1 ? info[1] : "";
18
19 // 获取文档组件路径
20 const nextToken = tokens[idx + 1];
21 const componentPath =
22 nextToken.type === "fence" ? nextToken.content : "";
23
24 let source = "";
25 if (componentPath) {
26 // /Users/dancy/Desktop/better-ui/docs/examples/button/basis.vue
27 let file = path.resolve(
28 __dirname,
29 "../examples",
30 `${componentPath}.vue`
31 );
32 file = file.replace(/\s+/g, "");
33 source = fs.readFileSync(file, "utf-8");
34 }
35 return `<demo path=${componentPath}>
36 <template #description>${description ? `${md.render(description)}` : ""}</template>
37 <template #source><pre><code class="language-html">${md.utils.escapeHtml(source)}</code></pre></template>
38 `;
39 } else {
40 return `</demo>`;
41 }
42 },
43 });
44 },
45 },
46});
渲染演示组件
docs/.vitepress/components.js
1const modulesFiles = import.meta.glob("../play/*/*.vue", {
2 eager: true,
3});
4
5// 自动化处理
6let modules = {};
7
8for (const [key, value] of Object.entries(modulesFiles)) {
9 const keys = key.split("/");
10 const name = keys.splice(1).join("/");
11 modules[name] = value.default;
12}
13
14export default modules;
docs/.vitepress/components/demo/index.vue
1<script setup lang="ts">
2import { computed } from "vue";
3import modules from "../../components";
4const props = defineProps({
5 path: {
6 type: String,
7 default: "",
8 },
9});
10
11const demo = computed(() => {
12 const key = `examples/${props.path}.vue`;
13 return modules[key];
14});
15
16console.log(props.path);
17console.log(demo);
18</script>
19
20<template>
21 <div class="examples-container">
22 <div class="description"><slot name="description" /></div>
23 <div class="examples-body">
24 <div class="examples-inner">
25 <component :is="demo" />
26 </div>
27 <div class="source-inner"><slot name="source" /></div>
28 </div>
29 </div>
30</template>
31
32<style lang="scss" scoped></style>
1pnpm add -D prismjs
.vitepress/components/demo/index.vue
为全局组件 Demo 引入 prism 组件包和样式
1<script setup lang="ts">
2import { computed, onMounted } from "vue";
3import prism from "prismjs";
4import "prismjs/themes/prism-tomorrow.min.css";
5import modules from "../../components";
6const props = defineProps({
7 path: {
8 type: String,
9 default: "",
10 },
11});
12
13const demo = computed(() => {
14 const key = `examples/${props.path}.vue`;
15 return modules[key];
16});
17
18onMounted(() => {
19 prism.highlightAll();
20});
21</script>
22
23<template>
24 <div class="examples-container">
25 <div class="description"><slot name="description" /></div>
26 <div class="examples-body">
27 <div class="examples-inner">
28 <component :is="demo" />
29 </div>
30 <div class="source-inner"><slot name="source" /></div>
31 </div>
32 </div>
33</template>
34
35<style lang="scss" scoped></style>
.vitepress/config.mts
为 code 标签添加类名 language-html
,指定 prismjs 对象以什么类型的语言高亮代码块
1import path from "node:path";
2import fs from "node:fs";
3import { defineConfig } from "vitepress";
4import MdContainer from "markdown-it-container";
5
6export default defineConfig({
7 markdown: {
8 // md 是 markdown-it 的实例
9 config: (md) => {
10 // markdown-it的use方法用于安装插件
11 md.use(MdContainer, "demo", {
12 render(tokens, idx) {
13 // 拿到tokens中的内容
14 if (tokens[idx].nesting === 1) {
15 // 获取:::demo后跟随的描述文本
16 const info = tokens[idx].info.trim().match(/^demo\s*(.*)$/);
17 const description = info && info.length > 1 ? info[1] : "";
18
19 // 获取文档组件路径
20 const nextToken = tokens[idx + 1];
21 const componentPath =
22 nextToken.type === "fence" ? nextToken.content : "";
23
24 let source = "";
25 if (componentPath) {
26 // /Users/dancy/Desktop/better-ui/docs/examples/button/basis.vue
27 let file = path.resolve(
28 __dirname,
29 "../examples",
30 `${componentPath}.vue`
31 );
32 file = file.replace(/\s+/g, "");
33 source = fs.readFileSync(file, "utf-8");
34 }
35 return `<demo path=${componentPath}>
36 <template #description>${description ? `${md.render(description)}` : ""}</template>
37 <template #source><pre><code class="language-html">${md.utils.escapeHtml(source)}</code></pre></template>
38 `;
39 } else {
40 return `</demo>`;
41 }
42 },
43 });
44 },
45 },
46});
.vitepress/theme/index.ts
1import './highlight.scss'
highlight.scss
1pre[class*="language-"] {
2 background-color: #fff;
3 margin: 0;
4 padding: 0 24px 24px;
5 border-radius: 12px;
6}
7
8.vp-doc [class*="language-"] code {
9 padding: 0;
10}
11
12.token {
13 &.attr-name,
14 &.deleted,
15 &.namespace,
16 &.tag {
17 color: #477aff;
18 }
19 &.attr-value,
20 &.char,
21 &.regex,
22 &.string,
23 &.variable {
24 color: #3cc17e;
25 }
26
27 &.atrule,
28 &.builtin,
29 &.important,
30 &.keyword,
31 &.selector {
32 color: #dd6c77;
33 }
34
35 &.boolean,
36 &.function,
37 &.number {
38 color: #dda03e;
39 }
40
41 &.punctuation {
42 color: #8a9bc4;
43 }
44}
复制功能使用第三方插件 clipboard
1pnpm add -D clipboard
1<script setup lang="ts">
2import { computed, ref, nextTick } from "vue";
3import modules from "../../components";
4import Clipboard from "clipboard";
5import prism from "prismjs";
6import "prismjs/themes/prism-tomorrow.min.css";
7
8const slots = defineSlots();
9const props = defineProps({
10 path: {
11 type: String,
12 default: "",
13 },
14});
15
16const source = ref(false);
17const sourceRef = ref(null);
18const isCopySuccess = ref(false);
19
20const demo = computed(() => {
21 const key = `examples/${props.path}.vue`;
22 return modules[key];
23});
24
25// 复制的图标元素
26const iconCopy = computed(() => {});
27// 复制组件源码
28const copyToClipboard = async (event) => {
29 // const textToCopy = slots.source()[0]?.children[0]?.children;
30
31 // navigator.clipboard
32 // .writeText(textToCopy)
33 // .then(function () {
34 // // 成功处理
35 // console.log('复制成功')
36 // })
37 // .catch(function (error) {
38 // // 错误处理
39 // console.log('复制失败')
40 // });
41 const clipboard = new Clipboard("#copy", {
42 text: () => slots.source()[0]?.children[0]?.children || "",
43 });
44 // 复制成功
45 clipboard.on("success", () => {
46 isCopySuccess.value = true;
47 clipboard.destroy();
48 });
49 // 复制失败
50 clipboard.on("error", () => {
51 isCopySuccess.value = false;
52 clipboard.destroy();
53 });
54};
55
56// 展开/收起组件源码
57const toggleSource = async () => {
58 source.value = !source.value;
59 await nextTick();
60 const children = sourceRef.value;
61 source.value && children && prism.highlightAllUnder(children);
62};
63</script>
64
65<template>
66 <div class="examples-container">
67 <!-- 描述 -->
68 <div class="description"><slot name="description" /></div>
69 <!-- 演示主体 -->
70 <div class="examples-body">
71 <!-- 组件渲染 -->
72 <div class="examples-inner">
73 <component :is="demo" />
74 </div>
75 <!-- 图标元素 -->
76 <div class="examples-control">
77 <!-- 复制 -->
78 <div
79 class="control-icon"
80 id="copy"
81 @click="copyToClipboard"
82 @mouseleave="isCopySuccess = false"
83 >
84 复制
85 </div>
86 <!-- 代码 -->
87 <div class="control-icon" @click="toggleSource">
88 展开
89 </div>
90 </div>
91 <!-- 组件源码 -->
92 <div v-if="source" ref="sourceRef" class="source-inner">
93 <slot name="source" />
94 </div>
95 </div>
96 </div>
97</template>
98
99<style lang="scss" scoped>
100.examples-body {
101 border: 1px solid #e4e4e7;
102 border-radius: 8px;
103}
104.examples-inner {
105 padding: 24px;
106}
107.examples-control {
108 text-align: center;
109 height: 43px;
110 display: flex;
111 align-items: center;
112 justify-content: center;
113 border-top: 1px dashed #e4e4e7;
114 margin: 0;
115 .control-icon {
116 cursor: pointer;
117 width: 20px;
118 height: 20px;
119 margin: 0 5px;
120 svg {
121 width: 100%;
122 height: 100%;
123 }
124 }
125}
126</style>