0.0.1 • Published 3 years ago

qing-ui-2 v0.0.1

Weekly downloads
-
License
UNLICENSED
Repository
-
Last release
3 years ago

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 示例)

  1. 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>
  1. 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'
      }
    ]
  ]
}
  1. docs/.vuepress/enhanceApp.js(入口文件)
import UI from '../../packages/index'

export default ({ Vue }) => {
  Vue.use(UI)
}
  1. 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>
  1. 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'
      }
    ]
  ]
}