qing-ui-2
v0.0.1
Published
克隆此仓库后运行:
Downloads
0
Readme
UI 组件库(vue2.0)
启动项目
克隆此仓库后运行:
# 安装依赖,推荐使用 yarn
$ npm install
# 打包组件库
$ npm run lib
# 启动测试用例
$ npm run servec
# 打包测试用例
$ npm run build
组件库目录结构
参考 ElementUI 目录结构
vue2.0github 源码地址
vue3.0github 源码地址
├── docs // 文档目录
├── examples // 测试示例
├── packages // 封装的所有组件
│ ├── styles // 全局样式
│ └── index.js // 导出组件配置
├── .gitignore // git 忽略项
├── .prettierrc // 代码格式化
├── babel.config.js // babel配置
├── vue.config.js // vue脚手架配置
├── README.md // 简介
└── package.json // package.json
技术栈选型
- 脚手架工具: @vue/cli
- 框架:vue2
- 语法:js
- CSS 语法 :scss
搭建组件库工程(搭建过程)
1. vue create 组件名包名称
上/下箭头移动光标,Enter 确认,空格 选择/取消选择
选择 Manually select features -> 选择 babel scss 进行安装
2. 调整目录(根目录)
1)新增packages
文件夹(存在封装的所有组件,打包也是对此文件进行打包)
2)src 文件夹更改为examples
(存放测试用例),更改完是跑不起来的,需配置。
项目根目录新建 vue.config.js
const path = require('path')
module.exports = {
pages: {
index: {
entry: 'examples/main.js', // 修改项目的入口文件
template: 'public/index.html',
filename: 'index.html'
}
},
// 扩展webpack 配置,使packages加入编译(高版本语法转低版本语法)
chainWebpack: (config) => {
config.module
.rule('js')
.include.add(path.resolve(__dirname, 'packages'))
.end()
.use('babel')
.loader('babel-loader')
.tap((options) => {
return options
})
}
}
3. 导出 vue 插件
packages目录下新建index.js,完整代码如下,其中引入的文件暂时不存在,样式文件不存在可暂时不引入
// 引入全局样式
import './styles/index.scss'
// 基础组件
import Button from './button/index'
// 存储组件列表
const components = [Button]
// 定义 install 方法,接口Vue作为参数。如果使用 use 注册插件,则所有的组件都会注册
const install = function (Vue) {
// 全局注册所有的组件
components.forEach((item) => {
Vue.component(item.name, item)
})
}
// 判断是否是直接引入文件,如果是就不用再调Vue.use(),像<script>直接使用也可以注册。
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
export default { install }
1) 做 vue 插件,需要了解install
用法:安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。 该方法需要在调用 new Vue() 之前被调用。 当 install 方法被同一个插件多次调用,插件将只会被安装一次。
- 也就是说如果要做一个插件,只需要导出 install 就好了.(这里导出 install 一个函数)
2) packages 目录新建 index.js(整个包的入口文件,一定要导出一个 install)
没有优化的 index.js
// index.js 这样的坏处是要一个一个注册。所以要数组存一下(也就是下面的components数组),在循环取出注册
import Button from './button/button.vue'
// 定义 install 方法,接口Vue作为参数。如果使用 use 注册插件,则所有的组件都会注册
const install = function (Vue) {
// 全局注册所有的组件
Vue.component(Button.name, Button)
}
export default install
优化后的 index.js
import Button from './button/button.vue'
// 存储组件列表
const components = [Button]
// 定义 install 方法,接口Vue作为参数。如果使用 use 注册插件,则所有的组件都会注册
const install = function (Vue) {
// 全局注册所有的组件
components.forEach((item) => {
Vue.component(item.name, item)
})
}
export default install
3) 如果不是模块化环境开发,像<script>,这些组件直接进行注册。就不用在调 use 方法。
// 判断是否是直接引入文件,如果是就不用再调Vue.use(),像<script>直接使用也可以注册。
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
5. packages 目录下新增 button 文件夹
参考目录如下:
├── packages
│ ├── button
| | ├── src
| | | ├──button.vue
│ └── index.js
参考内容如下:
// packages/index.js
import Button from './src/button'
export default Button
// packages/button/src/button.vue
<template>
<button>
<slot></slot>
</button>
</template>
<script>
export default {
name: 'demo-button'
}
</script>
4. 打包(packages 打包)
需要了解:vue cli 有一个构建目标成库
// 项目根目录 package.json "scripts" 下增加 "lib" 命令
{
"scripts": {
// "lib": "vue-cli-service build --target lib 打包路径"
"lib": "vue-cli-service build --target lib packages/index.js"
}
}
搭建组件库文档(搭建过程)
1. 配置文档结构
// 1. 根目录新增docs文件夹(文档目录)
// 2. 使用你喜欢的包管理器进行初始化
npm init -y
// 3.将 VuePress 安装为本地依赖
npm install -D vuepress
// 4. 将 命令配置为启动项,docs/packages.json
"scripts": {
"serve": "vuepress dev",
"build": "vuepress build"
},
// 4. 调整文件夹(此步配置完 就可以npm run serve查看基础文档了)
├── docs // 文档库
│ ├── .vuepress // vuepress配置路径(名字不能更换,注意大小写)
| | ├── components // 组件文件夹
| | | ├── button // button组件示例文件夹
| | | | ├── base.vue // button组件示例
| | | ├── demo-block.vue // button组件示例
| | ├── config.js // vuepress配置文件
| | ├── enhanceApp.js // 入口文件,引用写好的组件库
│ ├── components // 所有组件文档
│ | ├── button.md // button组件使用文档
│ | ├── install.md // 安装说明文档
│ | ├── introduce.md // 介绍文档
│ | ├── README.md // 组件使用文档首页
│ ├── README.md // 首页
// docs/.vuepress/config.js
module.exports = {
title: 'UI', // 网站名称
base: '/ui/',
description: '一个基于Vue.js的高质量桌面端组件库', // 简介
themeConfig: { // 导航
subSidebar: 'auto',
noFoundPageByTencent: false,
nav: [
{ text: 'Home', link: '/' },
{ text: '组件', link: '/components/install' }
],
sidebar: { // 侧边栏菜单
'/component': [
{
title: '开发指南',
collapsable: false,
children: ['/components/install', '/components/introduce']
},
{
title: '基础',
collapsable: false,
children: ['/components/button']
}
]
}
},
// 默认设置网站为中文
locales: {
'/': {
lang: 'zh-CN'
}
}
}
2. 编写组件示例(button 示例)
- docs/.vuepress/components/demo-block.vue(代码块组件,支持复制) 先复制,后期自我优化
<template>
<div
class="demo-block"
:class="[blockClass, { hover: hovering }]"
@mouseenter="hovering = true"
@mouseleave="hovering = false"
>
<div class="demo-content">
<slot name="source"></slot>
</div>
<div class="meta" ref="meta">
<div class="description" v-if="$slots.description">
<slot name="description"></slot>
</div>
<div class="code-content">
<slot></slot>
</div>
</div>
<div
class="demo-block-control"
:class="{ 'is-fixed': fixedControl }"
:style="{ width: fixedControl ? `${codeContentWidth}px` : 'unset' }"
ref="control"
@click="isExpanded = !isExpanded"
>
<transition name="arrow-slide">
<i :class="[iconClass, { hovering: hovering }, 'icon']"></i>
</transition>
<transition name="text-slide">
<span v-show="hovering">{{ controlText }}</span>
</transition>
<span
v-show="!copied"
:class="['copy-action', { 'copying ': copied }]"
@click.stop="copyCode"
>{{ copiedText }}</span
>
<transition name="bounce">
<span v-show="copied" class="copy-action copy-action-success">{{ copiedText }}</span>
</transition>
</div>
</div>
</template>
<script type="text/babel">
// import defaultLang from './i18n/default_lang.json'
const defaultLang = [
{
lang: 'zh-CN',
'demo-block': {
'hide-text': '隐藏代码',
'show-text': '显示代码',
'copy-text': '复制代码',
'copy-success': '复制成功'
}
},
{
lang: 'en-US',
'demo-block': {
'hide-text': '隐藏代码',
'show-text': '显示代码',
'copy-text': '复制代码',
'copy-success': '复制成功'
}
}
]
export default {
data() {
return {
hovering: false,
copied: false,
isExpanded: false,
fixedControl: false,
codeContentWidth: 0,
scrollParent: null
}
},
props: {
options: {
type: Object,
default: () => {
return {}
}
}
},
computed: {
compoLang() {
return this.options.locales || defaultLang
return this.options.locales
},
langConfig() {
return this.compoLang.filter((config) => config.lang === this.$lang)[0]['demo-block']
},
blockClass() {
return `demo-${this.$lang} demo-${this.$router.currentRoute.path.split('/').pop()}`
},
iconClass() {
return this.isExpanded ? 'caret-top' : 'caret-bottom'
},
controlText() {
return this.isExpanded ? this.langConfig['hide-text'] : this.langConfig['show-text']
},
copiedText() {
return this.copied ? this.langConfig['copy-success'] : this.langConfig['copy-text']
},
codeArea() {
return this.$el.getElementsByClassName('meta')[0]
},
codeAreaHeight() {
if (this.$el.getElementsByClassName('description').length > 0) {
return (
this.$el.getElementsByClassName('description')[0].clientHeight +
this.$el.getElementsByClassName('code-content')[0].clientHeight +
20
)
}
return this.$el.getElementsByClassName('code-content')[0].clientHeight
}
},
methods: {
copyCode() {
if (this.copied) {
return
}
const pre = this.$el.querySelectorAll('pre')[0]
pre.setAttribute('contenteditable', 'true')
pre.focus()
document.execCommand('selectAll', false, null)
this.copied = document.execCommand('copy')
pre.removeAttribute('contenteditable')
setTimeout(() => {
this.copied = false
}, 1500)
},
scrollHandler() {
const { top, bottom, left } = this.$refs.meta.getBoundingClientRect()
this.fixedControl =
bottom > document.documentElement.clientHeight &&
top + 44 <= document.documentElement.clientHeight
this.$refs.control.style.left = this.fixedControl ? `${left}px` : '0'
},
removeScrollHandler() {
this.scrollParent && this.scrollParent.removeEventListener('scroll', this.scrollHandler)
}
},
watch: {
isExpanded(val) {
this.codeArea.style.height = val ? `${this.codeAreaHeight + 1}px` : '0'
if (!val) {
this.fixedControl = false
this.$refs.control.style.left = '0'
this.removeScrollHandler()
return
}
setTimeout(() => {
this.scrollParent = document
this.scrollParent && this.scrollParent.addEventListener('scroll', this.scrollHandler)
this.scrollHandler()
}, 200)
}
},
mounted() {
this.$nextTick(() => {
let codeContent = this.$el.getElementsByClassName('code-content')[0]
this.codeContentWidth = this.$el.offsetWidth
if (this.$el.getElementsByClassName('description').length === 0) {
codeContent.style.width = '100%'
codeContent.borderRight = 'none'
}
})
},
beforeDestroy() {
this.removeScrollHandler()
}
}
</script>
<style scoped>
.demo-block {
border: solid 1px #ebebeb;
border-radius: 3px;
transition: 0.2s;
margin-top: 15px;
margin-bottom: 15px;
}
.demo-block.hover {
box-shadow: 0 0 8px 0 rgba(232, 237, 250, 0.6), 0 2px 4px 0 rgba(232, 237, 250, 0.5);
}
.demo-block code {
font-family: Menlo, Monaco, Consolas, Courier, monospace;
}
.demo-block .demo-button {
float: right;
}
.demo-block .demo-content {
padding: 24px;
}
.demo-block .meta {
background-color: #282c34;
border: solid 1px #ebebeb;
border-radius: 3px;
overflow: hidden;
height: 0;
transition: height 0.2s;
}
.demo-block .description {
padding: 20px;
box-sizing: border-box;
border: solid 1px #ebebeb;
border-radius: 3px;
font-size: 14px;
line-height: 22px;
color: #666;
word-break: break-word;
margin: 10px;
background-color: #fafafa;
}
.demo-block .demo-block-control {
border-top: solid 1px #eaeefb;
height: 44px;
box-sizing: border-box;
background-color: #fafafa;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
text-align: center;
margin-top: -1px;
color: #d3dce6;
cursor: pointer;
position: relative;
}
.demo-block .demo-block-control.is-fixed {
position: fixed;
bottom: 0;
width: 660px;
z-index: 999;
}
.demo-block .demo-block-control .icon {
font-family: element-icons !important;
font-style: normal;
font-weight: 400;
font-variant: normal;
text-transform: none;
line-height: 1;
vertical-align: baseline;
display: inline-block;
-webkit-font-smoothing: antialiased;
}
.demo-block .demo-block-control .caret-top::before {
content: '';
position: absolute;
right: 50%;
width: 0;
height: 0;
border-bottom: 6px solid #ccc;
border-right: 6px solid transparent;
border-left: 6px solid transparent;
}
.demo-block .demo-block-control .caret-bottom::before {
content: '';
position: absolute;
right: 50%;
width: 0;
height: 0;
border-top: 6px solid #ccc;
border-right: 6px solid transparent;
border-left: 6px solid transparent;
}
.demo-block .demo-block-control i {
font-size: 16px;
line-height: 44px;
transition: 0.3s;
}
.demo-block .demo-block-control i.hovering {
transform: translateX(-40px);
}
.demo-block .demo-block-control > span {
position: absolute;
transform: translateX(-30px);
font-size: 14px;
line-height: 44px;
transition: 0.3s;
display: inline-block;
}
.demo-block .demo-block-control .copy-action {
right: 0px;
color: #409eff;
}
.demo-block .demo-block-control.copying {
transform: translateX(-44px);
}
.demo-block .demo-block-control .copy-action-success {
color: #f5222d;
}
.demo-block .demo-block-control:hover {
color: #409eff;
background-color: #f9fafc;
}
.demo-block .demo-block-control .text-slide-enter,
.demo-block .demo-block-control .text-slide-leave-active {
opacity: 0;
transform: translateX(10px);
}
.demo-block .demo-block-control .bounce-enter-active {
animation: bounce-in 0.5s;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.5);
}
100% {
transform: scale(1);
}
}
.demo-block .demo-block-control .control-button {
line-height: 26px;
position: absolute;
top: 0;
right: 0;
font-size: 14px;
padding-left: 5px;
padding-right: 25px;
}
</style>
- docs/.vuepress/config.js 增加 plugins 配置(配置使用 demo-block 组件)
module.exports = {
plugins: [
[
'container',
{
type: 'demo',
before: (info) => `<demo-block><template slot="description">${info || ''}</template>\n`,
after: () => '</demo-block>\n'
}
]
]
}
- docs/.vuepress/enhanceApp.js(入口文件)
import UI from '../../packages/index'
export default ({ Vue }) => {
Vue.use(UI)
}
- docs/.vuepress/components/button/base.vue
因为 vue 中要使用 scss,所以需要独立安装。
在docs目录下安装
npm install sass sass-loader -D
使用中发现 scss-loader 版本过高会有问题(sass-loader is undefined)。可指定版本号为`10.1.0`,解决
如启动不起来,可以固定剩下项版本号为
"sass": "^1.32.8",
"sass-loader": "^10.1.0",
"vuepress": "^1.8.2"
<template>
<demo-button>111</demo-button>
</template>
<script>
export default {
name: 'button-base'
}
</script>
- docs/components/button.md (button 示例文档)
---
title: Button 按钮
---
## Button 组件
基础组件,触发业务逻辑时使用。
### 基础用法
::: demo 使用 type、plain、round 和 circle 属性来定义 Button 的样式。
<template v-slot:source>
<button-base />
</template>
<<< @/.vuepress/components/button/base.vue
:::
优化组件库文档UI
选型vuepress主题库
- vuepress-theme-reco
- AntDocs
- vuepress-theme-vdoing
# 采用 vuepress-theme-reco,使用人群大
# docs 目录下安装
npm install vuepress-theme-reco
配置vuepress-theme-reco
// docs/.vuepress/config.js 增加主题配置
module.exports = {
theme: 'reco'
}
docs/.vuepress/config.js 完整内容如下
module.exports = {
title: 'UI',
base: '/ui/',
description: '一个基于Vue.js的高质量桌面端组件库',
themeConfig: {
subSidebar: 'auto',
noFoundPageByTencent: false,
nav: [
{ text: 'Home', link: '/' },
{ text: '组件', link: '/components/install' }
],
sidebar: {
'/component': [
{
title: '开发指南',
collapsable: false,
children: ['/components/install', '/components/introduce']
},
{
title: '基础',
collapsable: false,
children: ['/components/button']
}
]
}
},
theme: 'reco',
// 默认设置网站为中文
locales: {
'/': {
lang: 'zh-CN'
}
},
plugins: [
[
'container',
{
type: 'demo',
before: info => `<demo-block><template slot="description">${info || ''}</template>\n`,
after: () => '</demo-block>\n'
}
]
]
}