仿element官网实现组件库可视化平台
实现方法和element官网不一致,仅参考页面结构。
写在前面:实现方法和element官网不一致,仅参考页面结构
需求分析:
1. 主体文档应该使用markdown编写,一个组件对应一个md文件,所以我们需要有在vue中导入md的功能。
2. 组件有不同的功能,需要提供一个演示框,这个演示框里面会放不同的组件功能展示,以及固定的查看源代码,复制代码功能。这个演示框应该是一个vue组件,所以需要有在md文件中导入vue组件的功能。
3、element右侧有一个目录,该目录需要自动提取md文件中的标题,需要跟着文档滚动而滚动,点击目录可以跳转到对应的标题
插件使用:
1、使用到vite-plugin-md vite的md插件,提供把md文件当做vue导入的能力,也可以在md文件中使用vue组件
2、prismjs 在代码展示的时候,提供代码高亮
3、markdown-it-anchor 自动提取md文件中的标题,实现右侧目录
路由设计:
在路由中引用的组件是一个md文件
文档主体:
每一个组件路由其实是一个md文件。要想在vue中正常解析.md需要下载一个vite插件。
npm i vite-plugin-md
代码高亮:
npm i prismjs
npm i vite-plugin-prismjs
vite.config.js配置如下:
右侧目录区:
npm i markdown-it-anchor
markdown-it-anchor插件可以用来实现右侧自适应目录,这个插件的作用是在vite-plugin-md插件生成vue组件时候,把h标签的内容作为它的id,这样就可以通过id跳转的方式从目录跳转到指定内容了。
vite.config.js配置如下:
右侧自适应目录需要拥有两个功能,一是点击目录页面滚动至对应内容,二是页面滚动时高亮对应目录
页面初始化时获取所有h2~h6标题内容渲染右侧目录,此时可知每个标题的对应id;
点击目录页面滚动至对应内容通过scrollTo方法实现即可;
页面滚动时高亮对应目录,需要在页面滚动时把所有标题按离视口的距离进行排序,过滤掉负数即可得到距离视口最近的标题元素id,根据id高亮对应目录
接下来就可以使用vue和md双向导入功能了。
vue导入md,直接导入作为组件就可以。
在md中使用vue组件的方法,md中可以用两种组件:
1. 全局组件 直接当html标签使用,可以直接解析
2. 局部组件,在md文件中导入使用,使用方式如下:
展示如下:
一共三个区域:
1. 展示区:通过slot,展示外部组件。
2. 控件区:查看源代码
3. 代码区:获取展示区传入的外部组件的代码,加上代码高亮展示
该组件(ShowCode)本身使用频繁,所以我们直接注册为全局组件,这样就可以直接在md文件中引入,而展示区的代码,则通过局部引入的方式,导入进行展示。
”隐藏源代码“使用sticky粘性定位,当代码区较长时也能展示在底部,方便使用。
每一个展示区,对应一个vue文件,这样控制粒度更加精细。
例:多个展示区时:
代码区:
在 Vite 的开发文档里有记载到,它支持在资源的末尾加上一个后缀来控制所引入资源的类型。比如可以通过
import xx from 'xx?raw' 以字符串形式引入 xx 文件。
但是这种方式只能在开发环境得到支持,所以生产环境需要换成网络请求的方式。
使用网络请求方式需要把page文件夹复制一份到打包后的根路径,使用copy-dir插件实现
在原本的打包命令后加上 && node ./config/copyDocs.js,就会自动在打包后执行这个代码,node后面是代码所在相对路径
代码实现:
使用了Tailwind CSS原子类,前缀是tw-
basic.vue:
<template>
<div class="page-content">
<v-sidebar tabName="components" />
<el-scrollbar
ref="containerRef"
class="tw-flex-1 tw-pr-[10px]"
height="100%"
@scroll.self="handleScroll"
>
<router-view></router-view>
</el-scrollbar>
<div class="cus-anchor cus-anchor--vertical tw-min-w-[150px]">
<div class="cus-anchor__marker" ref="markerRef"></div>
<div class="cus-anchor__list">
<div
class="cus-anchor__item"
v-for="item in catalogueData"
:key="item.id"
>
<a
@click.prevent
:id="`a-${item.id}`"
class="cus-anchor__link"
:class="{ is_active: curId === item.id }"
:href="`#${item.id}`"
@click="(e) => handleClick(item.id, e)"
>{{ item.text }}</a
>
</div>
</div>
</div>
</div>
</template>
<script setup>
import VSidebar from "./VSidebar.vue";
import { nextTick, onMounted, ref,onBeforeUnmount } from "vue";
import bus from '../bus/index'
const containerRef = ref(null)
bus.on('afterEach',()=>{
nextTick(()=>{
containerRef?.value?.setScrollTop(0);
markerRef.value.style.opacity = 0;
markerRef.value.style.top = "8px";
curId.value = null;
containerRef.value.update();
getElements();
nextTick(() => {
containerRef.value.handleScroll();
});
})
})
onBeforeUnmount(()=>{
bus.off('afterEach');
})
const catalogueData = ref([]);
const markerRef = ref(null);
const curId = ref();
const stopChange = ref(false);
onMounted(() => {
nextTick(() => {
containerRef.value.setScrollTop(0);
getElements();
nextTick(() => {
containerRef.value.handleScroll();
});
});
});
// 获取离视口最近的标题元素id
const getTopMore = (els) => {
const data = els
.map((item) => ({
offsetTop: item.getBoundingClientRect().top,
text: item.innerText,
id: item.id,
}))
.filter((item) => item.offsetTop > 0);
const minElement = data.sort((a, b) => a.offsetTop - b.offsetTop)[0];
return minElement?.id;
};
// 滚动时根据离视口最近的标题元素id高亮对应目录
const handleScroll = () => {
if (stopChange.value) return;
const hElement = document.querySelectorAll("h2,h3,h4,h5,h6");
const topEle = getTopMore([...hElement]);
if (curId.value !== topEle && topEle) {
curId.value = topEle;
markerRef.value.style.opacity = 1;
markerRef.value.style.top =
document.getElementById(`a-${curId.value}`).getBoundingClientRect().top -
64 +
"px";
}
};
// 获取所有标题元素
const getElements = () => {
const hElement = document.querySelectorAll("h2,h3,h4,h5,h6");
catalogueData.value = [...hElement].map((item) => {
return {
text: item.innerText,
id: item.id,
};
});
};
const handleClick = (anchor, e) => {
stopChange.value = true;
curId.value = anchor;
markerRef.value.style.opacity = 1;
markerRef.value.style.top = e.target.getBoundingClientRect().top - 64 + "px";
containerRef.value.scrollTo({
top: document.getElementById(anchor).offsetTop,
behavior: "smooth",
});
setTimeout(() => {
stopChange.value = false;
}, 1000);
};
</script>
<style lang="less" scoped>
.page-content {
display: flex;
height: calc(100% - 54px);
overflow: scroll;
}
.cus-anchor {
position: relative;
.cus-anchor__marker {
width: 4px;
height: 14px;
top: 8px;
left: 0;
transition:
top 0.25s ease-in-out,
opacity 0.25s;
position: absolute;
border-radius: 4px;
background-color: #45d4d5;
opacity: 0;
z-index: 0;
}
.cus-anchor__list {
padding-left: 14px;
.cus-anchor__item {
overflow: hidden;
display: flex;
flex-direction: column;
.cus-anchor__link {
display: inline-block;
font-size: 12px;
line-height: 22px;
padding: 4px 0;
color: #909399;
transition: color 0.3s;
white-space: nowrap;
text-decoration: none;
text-overflow: ellipsis;
overflow: hidden;
max-width: 100%;
outline: none;
cursor: pointer;
}
.is_active {
color: #45d4d5;
}
}
}
}
</style>
ShowCode.vue:
<template>
<div
class="tw-my-[10px] tw-border tw-border-[#dcdfe6] tw-box-border tw-rounded"
>
<div class="tw-box-border tw-p-[15px]">
<slot>
<el-button>测试一下</el-button>
</slot>
</div>
<div
class="tw-h-[35px] tw-border-t tw-border-[#dcdfe6] tw-flex tw-items-center tw-justify-end tw-p-[5px]"
>
<el-tooltip
:show-arrow="false"
:content="isShow ? '隐藏源代码' : '查看源代码'"
effect="dark"
placement="bottom-end"
>
<el-icon
class="tw-cursor-pointer tw-mx-[5px]"
@click="openAndCloseCode"
>
<Reading />
</el-icon>
</el-tooltip>
</div>
<transition
name="slide"
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
>
<div v-show="isShow" class="tw-box-border tw-relative">
<pre
style="margin: 0"
><code class="language-js line-numbers" style="padding-top: 0;">{{ sourceCode }}</code></pre>
<div
class="tw-border-t tw-border-[#dcdfe6] tw-mt-[-1px] tw-bg-[#fff] tw-z-10 tw-text-[#909399] hover:tw-text-[#409eff] tw-flex tw-justify-center tw-items-center tw-h-[44px] tw-cursor-pointer tw-sticky tw-bottom-0"
@click="openAndCloseCode"
>
<el-icon>
<CaretTop />
</el-icon>
<span class="tw-ml-[10px]">隐藏源代码</span>
</div>
</div>
</transition>
</div>
</template>
<script setup>
import { nextTick, onMounted, ref, useAttrs } from "vue";
import { Reading, CaretTop } from "@element-plus/icons-vue";
import Prism from "prismjs";
const { showpath } = useAttrs();
const sourceCode = ref();
onMounted(async () => {
const isDev = import.meta.env.MODE === "development";
let path = showpath || "button/baseComp";
if (isDev) {
/* @vite-ignore */
const data = await import(/* @vite-ignore */`/examples/src/page/${path}.vue?raw`);
sourceCode.value = data.default;
} else {
sourceCode.value = await fetch(`./page/${path}.vue`).then((res) =>
res.text()
);
}
await nextTick(() => {
Prism.highlightAll();
});
});
const isShow = ref(false);
// 展开控制
const openAndCloseCode = () => {
isShow.value = !isShow.value;
};
const beforeLeave = (el) => {
// 给元素设置过渡效果
el.style.transition = "0.3s height ease-in-out";
// 高度变化时,让其内容隐藏
el.style.overflow = "hidden";
};
const leave = (el) => {
el.style.height = "auto";
// 设置高度为具体的值
el.style.height = window.getComputedStyle(el).height;
// 强制浏览器回流,否则浏览器会合并两次元素的高度更改
el.offsetHeight;
el.style.height = "0px";
};
const afterLeave = (el) => {
// 收尾工作,展示完过渡效果之后,设为原来的值
el.style.transition = "";
el.style.overflow = "visible";
};
const beforeEnter = (el) => {
// 给元素设置过渡效果
el.style.transition = "0.3s height ease-in-out";
// 高度变化时,让其内容隐藏
el.style.overflow = "hidden";
};
const enter = (el) => {
el.style.height = "auto";
// 保存元素原来的高度
const endWidth = window.getComputedStyle(el).height;
el.style.height = "0px";
// 强制浏览器回流,否则浏览器会合并两次元素的高度更改
el.offsetHeight;
el.style.height = endWidth;
};
const afterEnter = (el) => {
// 收尾工作,展示完过渡效果之后,设为原来的值
el.style.transition = "";
el.style.overflow = "visible";
};
</script>
更多推荐
所有评论(0)