element ui select col 无效_从0开始,手把手带你打造自己的UI库(附文档)

从0开始,手把手带你打造自己的UI库(附文档)

前言

本篇文章是为了锻炼自己的技术能力还有底子,模仿element-ui进行开发的UI库。纯属学习使用。本文利用Vue-Cli4进行构建。css预编译器用的sass

文档地址这里

说一下文档。进去会有点慢。

  1. 服务器原因。
  2. 没有打包 启动的node服务。(打包因为使用了vue组件,所以出现错误。目前我还不会解决。有大能可以帮忙解决一下最好)

github地址 这里

本次大大小小总共写了 12 组件。分别是

  1. Button组件
  2. Layout 布局组件
  3. Container 容器组件
  4. input 输入框组件
  5. Upload 上传组件
  6. DatePick 日历组件
  7. Switch 开关组件
  8. infinteScroll 无线滚动指令
  9. Message 通知组件
  10. Popover 弹出框组件
  11. 分页组件
  12. table 表格组件

大概就这么多。废话不多说,接下来开始进行每个组件的解析和创建

代码结构

ui

|-- undefined
    |-- .browserslistrc
    |-- .editorconfig
    |-- .eslintrc.js
    |-- .gitignore
    |-- babel.config.js
    |-- karma.conf.js  //karma 配置
    |-- package-lock.json
    |-- package.json
    |-- packeage解释.txt
    |-- README.md
    |-- services.js  // 文件上传服务器
    |-- vue.config.js
    |-- dist // 打包后
    |   |-- ac-ui.common.js
    |   |-- ac-ui.common.js.map
    |   |-- ac-ui.css
    |   |-- ac-ui.umd.js
    |   |-- ac-ui.umd.js.map
    |   |-- ac-ui.umd.min.js
    |   |-- ac-ui.umd.min.js.map
    |   |-- demo.html
    |-- public
    |   |-- 1.html
    |   |-- favicon.ico
    |   |-- index.html
    |-- src  // 主文件夹
    |   |-- App.vue
    |   |-- main.js
    |   |-- assets
    |   |   |-- logo.png
    |   |-- components   // 测试用例
    |   |   |-- ButtonTest.vue
    |   |   |-- ContainerTest.vue
    |   |   |-- DatePickTest.vue
    |   |   |-- FormTest.vue
    |   |   |-- InfiniteScrollTest.vue
    |   |   |-- LayoutTest.vue
    |   |   |-- MessageTest.vue
    |   |   |-- paginationTest.vue
    |   |   |-- PopoverTest.vue
    |   |   |-- SwitchTest.vue
    |   |   |-- TableTest.vue
    |   |-- packages // UI
    |   |   |-- index.js
    |   |   |-- infiniteScroll.js
    |   |   |-- progress.vue
    |   |   |-- button
    |   |   |   |-- Button.vue
    |   |   |   |-- ButtonGroup.vue
    |   |   |   |-- Icon.vue
    |   |   |-- container
    |   |   |   |-- aside.vue
    |   |   |   |-- container.vue
    |   |   |   |-- footer.vue
    |   |   |   |-- header.vue
    |   |   |   |-- main.vue
    |   |   |-- datePack
    |   |   |   |-- date-pick.vue
    |   |   |   |-- date-range-pick.vue
    |   |   |-- Form
    |   |   |   |-- ajax.js
    |   |   |   |-- input.vue
    |   |   |   |-- upLoad-drag.vue
    |   |   |   |-- upLoad.vue
    |   |   |-- layout
    |   |   |   |-- Col.vue
    |   |   |   |-- Row.vue
    |   |   |-- Message
    |   |   |   |-- index.js
    |   |   |   |-- Message.vue
    |   |   |-- pagination
    |   |   |   |-- pagination.vue
    |   |   |-- popover
    |   |   |   |-- popover.vue
    |   |   |-- switch
    |   |   |   |-- Switch.vue
    |   |   |-- Table
    |   |       |-- Table.vue
    |   |-- styles // 全局样式
    |       |-- icon.js
    |       |-- mixin.scss
    |       |-- _var.scss
    |-- tests // 测试用例
    |   |-- button.spec.js
    |   |-- col.spec.js
    |-- uploads   // 文件上传路径
        |-- 1.js

通用代码

样式

// styles/_var
$border-radius: 4px;

$primary: #409EFF;
$success: #67C23A;
$warning: #E6A23C;
$danger: #F56C6C;
$info: #909399;

$primary-hover: #66b1ff;
$success-hover: #85ce61;
$warning-hover: #ebb563;
$danger-hover: #f78989;
$info-hover: #a6a9ad;

$primary-active: #3a8ee6;
$success-active: #5daf34;
$warning-active: #cf9236;
$danger-active: #dd6161;
$info-active: #82848a;

$primary-disabled: #a0cfff;
$success-disabled: #b3e19d;
$warning-disabled: #f3d19e;
$danger-disabled: #fab6b6;
$info-disabled: #c8c9cc;

$--xs: 767px !default;
$--sm: 768px !default;
$--md: 992px !default;
$--lg: 1200px !default;
$--xl: 1920px !default;

$map: (
"xs":(max-width:$--xs),
"sm":(min-width:$--sm),
"md":(min-width:$--md),
"lg":(min-width:$--lg),
"xl":(min-width:$--xl),
);
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}

混入函数

//flex布局复用
@import "var";
@mixin flexSet($dis:flex,$hov:space-between,$ver:middle,$col:center) {
display: $dis;
justify-content: $hov; // 主轴对齐方式
align-items: $col;
vertical-align: $ver // 图片对其
};


@mixin position($pos:absolute,$top:0,$left:0,$width:100%,$height:100%){
position: $pos;
top: $top;
left: $left;
width: $width;
height: $height;
};


@mixin res($key) {
// inspect Map 无法转换为纯 CSS。使用一个作为 CSS 函数的变量或参数的值将导致错误。使用inspect($value)函数来生成一个对调试 map 有用的输出字符串。
@media only screen and #{inspect(map_get($map,$key))}{
@content //插槽
}
}

Button 组件

Button

首先要确认的是,Button都有哪些常用的属性

  1. type 类型,分别控制按钮不同的颜色
  2. icon 字体图标。看按钮是否要带有图标
  3. iconPosition 字体图标的位置。
  4. loading 加载状态
  5. disable  和 loading 一起控制
  6. 以下没有实现,感觉比较简单。所以偷个懒
  7. size 按钮大小   (这里我就偷懒了,感觉这个比较好实现)
  8. radio 圆角  也就是加一个 border-radius

暂时就想到这么多。先实现把

html结构

<template>
  <button class="ac-button" :class="btnClass" :disabled="loading" @click="$emit('click',$event)">
    <ac-icon v-if="icon  && !loading" :icon="icon" class="icon">ac-icon>
    <ac-icon v-if="loading" icon="xingzhuang" class="icon">ac-icon>
    <span v-if="this.$slots.default">
      <slot>slot>
    span>
  button>
template>

这段代码应该比较容易理解。注意点

  1. 我是利用order来进行 图标位置的前后,也可以再span后面在加上一个ac-iconif判断即可
  2. @click 事件是需要触发 父级click事件。如果有其他需要还可以继续添加

JS部分




ButtonGroup

这个就比较简单了。就是利用插槽,内容填充一下。然后更改一下样式即可。

当然 也可以写一个报错信息






Layout 布局组件

参考element-ui,有两个组件。

  1. 一个row 代表行
  2. 一个col 代表列

分析一下行的作用,控制元素的 排列方式,元素直接的距离等,再把里面内容展现出来

列的作用 需要控制自己所占大小,偏移。响应等

接下来开始实现。

row






html结构简单,就是把传入的呈现出来。props方面也比较简单,有一个 自定义校验器。前面也说过了。解释一下其他的

  1. mounted 。里面 获取所有子元素,吧gutter赋给他们
  2. ...style 为什么要解构,防止里面有样式
  3. 这里直接使用了 flex布局。有精力得小伙伴可以再补充一下浮动

col






这段代码的核心就是:通过计算属性把不同的class给加入到组件上

关于下面的 res 再上面通用代码里。就是一些sass的应用

Container 容器组件

容器组件就相对来说简单了。就是利用H5新标签。

里面使用了flex

aside





main







header







footer







container






input 输入框组件

参考element,应该有以下功能

  1. 可情况
  2. 密码展示
  3. 带图标的输入框
  4. 状态禁用
v-if="prefixIcon"
> :value="value"
@input="$emit('input',$event.target.value)"
:disabled="disabled" ref="input"
@change="$emit('change',$event)"
@blur="$emit('blur',$event)"
@focus="$emit('focus',$event)"
> v-if="clearable && value"
@click.native="$emit('input','')"
@mousedown.native.prevent
> v-if="ShowPassword && value"
@click.native="changeState"
> v-if="suffixIcon"
>






先看以下html的代码结构发现并不难,利用v-if控制 ac-icon的隐藏。利用props传入属性来控制。计算属性控制class的添加

特别注意。记得在组件上写@xxx="$emit('xxx',$event)"。否则父类触发不了事件

Upload 上传组件

html结构

ref="input" @change="handleChange">

{{ file.name }}

upLoadDrag在后面拖拽上传

type = file时 参考

解释一下 html结构

  1. 根据传入drag,判断是否需要拖拽上传
  2. 文件列表。根据不同的状态来决定是否显示progress

js css 结构

css就几行。所以就直接写在这里面了

props解释

  1. name 输入框提交到后台的名字
  2. action  提交地址
  3. :limit 限制提交个数
  4. accept 类型
  5. :on-exceed  超过提交个数  会执行次方法
  6. :on-change 上传文件发生状态变化 会触发  选择文件 上传成功等
  7. :on-success  上传成功时候触发
  8. :on-error     上传失败时候触发
  9. :on-progress  上传过程中时候触发
  10. :before-upload 上传之前触发的函数
  11. :file-list  上传文件列表
  12. httpRequest  提供上传方法,例如aixos  默认ajax

JS可能这一长串代码可能看的会头疼。我先来串一下流程。

  1. 首先把input 隐藏。点击 div。触发handleClick方法,作用清空值,并且click input
  2. 选择文件后触发change handleChange事件。获取文件列表, 开始准备上传
  3. uploadFiles方法,获取文件个数,通过handleFormat格式化文件,然后通过upload上传
  4. upload 判断是否有beforeUpload传入,传入执行,没有就上传
  5. post 整合参数,开始上传。



拖拽上传

相比上面,这里面就是改了一些 把click 改成了drop

还有一些文件


@dragover.prevent
@dragleave.prevent
>将文件拖拽到此区域

原生ajax

export default function ajax(options) {
  // 创建 对象
  const xhr = new XMLHttpRequest()
  const action = options.action

  const fd = new FormData() // H5上传文件API
  fd.append(options.fileName,options.file)
  // console.log(options.fileName,options.file)

  // console.log('文件名'+options.fileName,options.file)

  xhr.onerror = function (err){
    options.onError(err) // 触发错误回调
  }

  // 上传完毕后走这个方法 H5 api
  xhr.onload = function (){
    let text = xhr.response || xhr.responseText
    options.onSuccess(JSON.parse(text))
  }

  xhr.upload.onprogress = function(e){
    if (e.total > 0){
      e.percent = e.loaded/e.total * 100
    }
    options.onProgress(e)
  }

  // 开启清求
  xhr.open('post',action,true)

  // 发送清求
  xhr.send(fd)
  return xhr
}

DatePick 日历组件

日历组件的 结构不是很难。难得是 要去计算时间

思路解释一下

  1. input聚焦后,执行handleFocus函数,显示下面得日历框。点击div外面。执行handleBlur。关闭日历框
  2. 接下来是content的里面的。显示头部,4个icon 外加时间显示
  3. 接下来时日历和时间

最主要难得就时时间的显示。得一步一步算。

每个人的计算方式不一样。这里只给一个参照。

@change="handleChange">
{{ TemTime.year }}{{ TemTime.month+1 }}
{{ week }}
@click="selectDay(getCurrentMonth(i,j))"
:class="{
isNotCurrentMonth: !isCurrentMonth(getCurrentMonth(i,j)),
isToday:isToday(getCurrentMonth(i,j)),
isSelect:isSelect(getCurrentMonth(i,j))
}">
{{getCurrentMonth(i,j).getDate()}}
{{ this.TemTime.year }}
{{ i }}
{{ startYear() }}年-{{ startYear()+10 }}
@click="setYear(i)"
>{{ i.getFullYear() }}





Switch 开关组件

switch就相对简单一点。纯样式控制。input写到 label内,不需要写for了。通过伪类控制。

通过computed来控制class样式添加

{{ activeText }}{{ inactiveText }}





infinteScroll 无限滚动指令

无限滚动不能作为一个组件。所以放成一个指令。参考地址

  1. attributes 自定义的默认属性
  2. getScrollContainer  获取Scroll的容器元素
  3. getScrollOptions  属性合并
  4. handleScroll 控制是否Scroll

思路。插入的时候 获取fnvnode.再获取容器。获取参数。绑定事件。最后解除绑定

重点说一下  MutationObserver  MDN

import throttle from 'lodash.throttle'
// 自定义属性
const attributes = {
  delay: {
    default: 200
  },
  immediate: {
    default: true
  },
  disabled: {
    default: false
  },
  distance: {
    default: 10
  },

}


/**
 *  获取Scroll的容器元素
 * @param el 元素节点
 * @returns {(() => (Node | null))|ActiveX.IXMLDOMNode|(Node & ParentNode)|Window}
 */
const getScrollContainer = (el)=>{
  let parent = el
  while (parent) {
    if (document.documentElement === parent) {
      return window
    }
    // 获取元素是否有 overflow属性
    const overflow = getComputedStyle(parent)['overflow-y']
    if (overflow.match(/scroll|auto/)) {
      return parent
    }
    parent = parent.parentNode
  }
}

/**
 * 拿到传入的属性和默认属性进行比对  合并
 * @param el 节点
 * @param vm  Vue实例
 * @returns {{}}  合并后的属性
 */
const getScrollOptions = (el, vm)=>{
  // entries参考网址 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
  return Object.entries(attributes).reduce((map, [key, option])=>{
    let defaultValue = option.default
    let userValue = el.getAttribute(`infinite-scroll-${ key }`)
    map[key] = vm[userValue] ? vm[userValue] : defaultValue
    return map
  }, {})
}

const handleScroll = function(cb) {
  let { container, el, vm,observer } = this['infinite-scroll'] // 绑定了this
  let { disabled,distance } = getScrollOptions(el, vm)
  if (disabled) return
  let scrollBottom = container.scrollTop + container.clientHeight
  if (container.scrollHeight - scrollBottom <= distance){
    cb()
  }else {
    if (observer){ // 接触监控
      observer.disconnect()
      this['infinite-scroll'].observer = null
    }
  }
}

export default {
  name: 'infinite-scroll',
  
  inserted(el, bindings, vNode) { // vNode里面有context可以访问上下文
    // 插入 指令生效
    console.log('指令生效')
    console.log(bindings.value) // 获取到fn
    console.log(vNode.context) // 获取虚拟实例 里面有属性
    let cb = bindings.value
    let vm = vNode.context
    // 1. 开始寻找循环的容器
    let container = getScrollContainer(el)
    console.log(container)
    if (container !== window) {
      console.log('绑定事件')
      // 2. 获取Options
      let { delay, immediate } = getScrollOptions(el, vm)
      // 3. 执行函数 节流 增加滚动事件
      let onScroll = throttle(handleScroll.bind(el, cb), delay)
      el['infinite-scroll'] = {
        container,
        onScroll, el, vm
      }
      if (immediate) {
        const observe =el['infinite-scroll'].observer= new MutationObserver(onScroll)  // 观察页面是否继续加载
        observe.observe(container, {
          childList: true,  // 监控孩子列表发生变化
          subtree: true  // 当子dom元素 发生变化也触发
        })
        onScroll() // 默认先加载
      }

      container.addEventListener('scroll', onScroll)
    }
  },

  unbind(el) {
    // 解除
    const { container, onScroll } = el
    if (container) {
      container.removeEventListener('scroll', onScroll)
      el['infinite-scroll'] = {}
    }
  }
}

Message 通知组件

这里面有两个。为什么又两个,因为message是通过appendChild添加到Dom里面的

思路

  1. 通过extend方法生成一个vue子类。然后通过$mount生成dom对象再添加到document
  2. options.closeelement方法里不是这样写的还有一部分判断等。这里接直接偷懒了,能正常使用

index

  1. 因为可能要有多个message。需要计算高度。所以使用了 数组存放。根据个数循环高度
import Vue from 'vue'
import MessageCom from './Message.vue';

let instances = []
// 生成一个vue 的 子类
let MessageConstructor = Vue.extend(MessageCom)

// 参考element 的写法  做了一定的修改和简化
const Message = (options)=>{
  options.close = function() {
    let length = instances.length
    instances.splice(0, 1);
    for (let i = 0; i 1; i++) {
      let removedHeight = instances[i].$el.offsetHeight;
      let dom = instances[i].$el;
      dom.style['top'] =
        parseInt(dom.style['top'], 10) - removedHeight - 16 + 'px';
    }
  }
  let instance = new MessageConstructor({
    data: options,
  })
  instance.$mount()
  document.body.appendChild(instance.$el)

  let verticalOffset = 20;
  instances.forEach(item=>{
    verticalOffset += item.$el.offsetHeight + 16;  // 53 +16
  });
  instance.verticalOffset = verticalOffset;
  instance.visible = true
  instances.push(instance)
  return instance
}

// 加载 'warning', 'error', 'success', 'info' 等
['warning', 'error', 'success', 'info'].forEach(type=>{
  Message[type] = function(options) {
    options.type = type
    return Message(options)
  }
})


export default Message

message

这个里面没有什么较难的内容。基本就是样式的控制


:style="messageStyle"
:
>
{{ message }}

Popover 弹出框组件

这个组件跟Message差不多。并不难。主要对JS三大家族的的引用。获得元素位置。根据元素位置来确定popover的位置

@click.stop阻止事件冒泡

个人觉得这一部分写的有一点冗余。感觉可以用offset搞定全部的。但是没有使用。就先这样吧


:
:style="position"
ref="content"
@click.stop>

{{ title }}

{{ content }}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值