UI组件库解析

定义文档组件

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>