從0開始,手把手帶你打造本身的UI庫(附文檔)

從0開始,手把手帶你打造本身的UI庫(附文檔)

前言

本篇文章是爲了鍛鍊本身的技術能力還有底子,模仿element-ui進行開發的UI庫。純屬學習使用。本文利用Vue-Cli4進行構建。css預編譯器用的sasscss

文檔地址這裏html

說一下文檔。進去會有點慢。前端

  1. 服務器緣由。
  2. 沒有打包 啓動的node服務。(打包由於使用了 vue組件,因此出現錯誤。目前我還不會解決。有大能能夠幫忙解決一下最好)

github地址 這裏vue

本次大大小小總共寫了 12 組件。分別是node

  1. Button組件
  2. Layout 佈局組件
  3. Container 容器組件
  4. input 輸入框組件
  5. Upload 上傳組件
  6. DatePick 日曆組件
  7. Switch 開關組件
  8. infinteScroll 無線滾動指令
  9. Message 通知組件
  10. Popover 彈出框組件
  11. 分頁組件
  12. table 表格組件

大概就這麼多。廢話很少說,接下來開始進行每一個組件的解析和建立python

代碼結構

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都有哪些經常使用的屬性css3

  1. type 類型,分別控制按鈕不一樣的顏色
  2. icon 字體圖標。看按鈕是否要帶有圖標
  3. iconPosition 字體圖標的位置。
  4. loading 加載狀態
  5. disable  和 loading 一塊兒控制
  6. 如下沒有實現,感受比較簡單。因此偷個懶
  7. size 按鈕大小   (這裏我就偷懶了,感受這個比較好實現)
  8. radio 圓角  也就是加一個 border-radius

暫時就想到這麼多。先實現把git

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>

這段代碼應該比較容易理解。注意點github

  1. 我是利用 order來進行 圖標位置的先後,也能夠再 span後面在加上一個 ac-iconif判斷便可
  2. @click 事件是須要觸發 父級 click事件。若是有其餘須要還能夠繼續添加

JS部分

<script>
  export default {
    name'ac-button',
    props: {
      type: {
        typeString,
        default'',
        validator(type) {
          if (type && !['waring''success''danger''info''primary'].includes(type)) {
            console.error('type類型必須是' + ['waring''success''danger''info''primary'].join(','))
          }
          return true
        }
      },
      icon: {
        typeString
      },
      iconPosition: {
        typeString,
        default'left',
        validator(type) {
          if (type && !['left''right'].includes(type)) {
            console.error('type類型必須是' + ['left''right'].join(','))
          }
          return true
        }
      },
      loading: {
        typeBoolean,
        defaultfalse
      }
    },
    computed: {
      btnClass() {
        const classes = []
        if (this.type) {
          classes.push(`ac-button-${ this.type }`)
        }
        if (this.iconPosition) {
          classes.push(`ac-button-${ this.iconPosition }`)
        }
        return classes
      }
    }
  }
</script>

js部分這裏面也好理解。主要解釋一下如下部分web

  1. validator,自定義校驗器 參考文檔
  2. computed  根據傳入屬性動態綁定 class,有好幾種方法,這裏只是其中一種

css部分

<style lang="scss">
@import "../../styles/var";
@import "../../styles/mixin";

$height: 42px;
$font-size: 16px;
$color: #606266;
$border-color: #dcdfe6;
$background: #ecf5ff;
$active-color: #3a8ee6;
.ac-button {
border-radius: $border-radius;
border: 1px solid $border-color;
height: $height;
color: $color;
font-size: $font-size;
line-height: 1;
cursor: pointer;
padding: 12px 20px;
@include flexSet($dis: inline-flex, $hov: center);
user-select: none; // 是否能夠選中文字
&:hover, &:focus {
color: $primary;
border-color: $border-color;
background-color: $background;
}

&:focus {
outline: none;
}

&:active {
color: $primary-active;
border-color: $primary-active;
background-color: $background;
}

@each $type, $color in (primary:$primary, success:$success, danger:$danger, waring:$warning, info:$info) {
&-#{$type} {
background-color: $color;
border: 1px solid $color;
color: #fff;
}
}

@each $type, $color in (primary:$primary-hover, success:$success-hover, danger:$danger-hover, waring:$warning-hover, info:$info-hover) {
&-#{$type}:hover, &-#{$type}:focus {
background-color: $color;
border: 1px solid $color;
color: #fff;
}
}

@each $type, $color in (primary:$primary-active, success:$success-active, danger:$danger-active, waring:$warning-active, info:$info-active) {
&-#{$type}:active {
background-color: $color;
border: 1px solid $color;
color: #fff;
}
}

@each $type, $color in (primary:$primary-disabled, success:$success-disabled, danger:$danger-disabled, waring:$warning-disabled, info:$info-disabled) {
&-#{$type}[disabled] {
cursor: not-allowed;
color: #fff;
background-color: $color;
border-color: $color;
}
}

.icon {
width: 16px;
height: 16px;
}

&-left {
svg {
order: 1
}

span {
order: 2;
margin-left: 4px;
}
}

&-right {
svg {
order: 2
}

span {
order: 1;
margin-right: 4px;
}
}
}
</style>

cssbutton樣式相對比較簡單。

提一下Sass @each用法。參考文檔

就是一個循環,能夠循環 數組 或者 對象,相似 pythonfor循環

Icon

這個也比較簡單 就直接上代碼了

<template>
<svg class="ac-icon" aria-hidden="true" @click="$emit('click')">
<use :xlink:href="`#icon-${icon}`"></use>
</svg>
</template>

<script>
import '../../styles/icon.js'

export default {
name: 'ac-icon',
props:{
icon:{
type: String,
require: true
}
}
}
</script>

<style lang="scss">
.ac-icon {
width: 25px;
height:25px;
vertical-align: middle;
fill: currentColor;
}
</style>

ButtonGroup

這個就比較簡單了。就是利用插槽,內容填充一下。而後更改一下樣式便可。

固然 也能夠寫一個報錯信息

<template>
<div class="ac-button-group">
<slot></slot>
</div>
</template>

<script>
export default {
name: 'ac-button-group',
mounted() {
let children = this.$el.children
for (let i = 0; i < children.length; i++) {
console.assert(children[i].tagName === 'BUTTON','子元素必須是button')
}
}
}
</script>

<style scoped lang="scss">
@import "../../styles/mixin";
@import "../../styles/var";
.ac-button-group{
@include flexSet($dis:inline-flex);
button{
border-radius: 0;
&:first-child{
border-top-left-radius: $border-radius;
border-bottom-left-radius: $border-radius;
}
&:last-child{
border-top-right-radius: $border-radius;
border-bottom-right-radius: $border-radius;
}
&:not(first-child){
border-left: none;
}
}
}
</style>

Layout 佈局組件

參考element-ui,有兩個組件。

  1. 一個 row 表明行
  2. 一個 col 表明列

分析一下行的做用,控制元素的 排列方式,元素直接的距離等,再把裏面內容展示出來

列的做用 須要控制本身所佔大小,偏移。響應等

接下來開始實現。

row

<template>
<div class="ac-row" :style="rowStyle">
<slot></slot>
</div>
</template>

<script>
export default {
name: 'ac-row',
props:{
gutter:{
type:Number,
default:0
},
justify:{
type: String,
validator(type){
if (type && !['start', 'end', 'content', 'space-around', 'space-between'].includes(type)) {
console.error('type類型必須是' + ['start', 'end', 'content', 'space-around', 'space-between'].join(','))
}
return true
}
}
},
mounted() {
this.$children.forEach(child=>{
child.gutter = this.gutter
})
},
computed:{
rowStyle(){
let style={}
if (this.gutter){
style = {
...style,
marginLeft: -this.gutter/2 + 'px',
marginRight: -this.gutter/2 + 'px'
}
}
if (this.justify){
let key = ['start','end'].includes(this.justify)?`flex-${this.justify}`:this.justify
style = {
...style,
justifyContent:key
}
}
return style
}
}
}
</script>

<style lang="scss">
.ac-row{
display: flex;
flex-wrap: wrap;
overflow: hidden;
}
</style>

html結構簡單,就是把傳入的呈現出來。props方面也比較簡單,有一個 自定義校驗器。前面也說過了。解釋一下其餘的

  1. mounted 。裏面 獲取全部子元素,吧gutter賦給他們
  2. ...style 爲何要解構,防止裏面有樣式
  3. 這裏直接使用了 flex佈局。有精力得小夥伴能夠再補充一下浮動

col

<template>
<div class="ac-col" :class="colClass" :style="colStyle">
<slot></slot>
</div>
</template>

<script>
export default {
name: 'ac-col',
data(){
return {
gutter:0
}
},
props:{
span:{
type:Number,
default:24
},
offset:{
type: Number,
default: 0
},
xs:[Number,Object],
sm:[Number,Object],
md:[Number,Object],
lg:[Number,Object],
xl:[Number,Object],
},
computed:{
colClass(){
let classes = []
classes.push(`ac-col-${this.span}`)
if (this.offset){
classes.push(`ac-col-offset-${this.offset}`)
}
['xs','sm','md','lg','xl'].forEach(type =>{
if (typeof this[type] === 'object'){
let {span,offset} = this[type]
span && classes.push(`ac-col-${type}-${span}`) // ac-col-xs-1
offset && classes.push(`ac-col-${type}-offset-${offset}`) // ac-col-xs-offset-1
}else {
//ac-col-xs-1
this[type] && classes.push(`ac-col-${type}-${this[type]}`)
}
})
return classes
},
colStyle(){
let style={}
if (this.gutter){
style = {
...style,
paddingLeft: this.gutter/2 + 'px',
paddingRight: this.gutter/2 + 'px'
}
}
return style
}
}
}
</script>

<style lang="scss">
/*經過循環24來創造寬度 sass語法*/
@import "./../../styles/_var";
/* 百分比佈局*/
@import "./../../styles/mixin";
@for $i from 1 through 24{
.ac-col-#{$i}{
width: $i/24*100%;
}
.ac-col-offset-#{$i}{
margin-left: $i/24*100%;
}
}
/*響應式佈局*/
@each $key in ('xs','sm','md','lg','xl'){
@for $i from 1 through 24{
@include res($key){
.ac-col-#{$key}-#{$i}{
width: $i/24*100%;
}
}
}
}
</style>

這段代碼的核心就是:經過計算屬性把不一樣的class給加入到組件上

關於下面的 res 再上面通用代碼裏。就是一些sass的應用

Container 容器組件

容器組件就相對來講簡單了。就是利用H5新標籤。

裏面使用了flex

aside

<template>
<aside class="ac-aside" :style="`width:${width}`">
<slot></slot>
</aside>
</template>

<script>
export default {
name: 'ac-aside',
props: {
width: {
type: String,
default: '300px'
}
}
}
</script>

main

<template>
<main class="ac-main">
<slot></slot>
</main>
</template>

<script>
export default {
name: 'ac-main'
}
</script>

<style lang="scss">
.ac-main{
flex: 1;
padding: 20px;
}
</style>

header

<template>
<header class="ac-header" :style="height">
<slot></slot>
</header>
</template>

<script>
export default {
name: 'ac-header',
props: {
height: {
type: String,
default: '60px'
}
}
}
</script>

<style lang="scss">
.ac-header {

}
</style>

footer

<template>
<footer class="ac-footer" :style="height">
<slot></slot>
</footer>
</template>

<script>
export default {
name: 'ac-footer',
props: {
height: {
type: String,
default: '60px'
}
}
}
</script>

<style>
.ac-footer {

}
</style>

container

<template>
<section class="ac-container" :class="{isVertical}">
<slot></slot>
</section>
</template>

<script>
export default {
name: 'ac-container',
data() {
return {
isVertical: true
}
},
mounted() {
this.isVertical = this.$children.some(child=>
["ac-header", "ac-footer"].includes(child.$options.name)
)
}
}
</script>

<style lang="scss">
.ac-container {
display: flex;
flex-direction: row;
flex: 1;
}

.ac-container.isVertical {
flex-direction: column;
}

</style>

input 輸入框組件

參考element,應該有如下功能

  1. 可狀況
  2. 密碼展現
  3. 帶圖標的輸入框
  4. 狀態禁用
<template>
<div class="ac-input" :class="elInputSuffix">
<ac-icon :icon="prefixIcon"
v-if="prefixIcon"
></ac-icon>
<input :type="ShowPassword?(password?'password':'text'):type" :name="name" :placeholder="placeholder"
:value="value"
@input="$emit('input',$event.target.value)"
:disabled="disabled" ref="input"
@change="$emit('change',$event)"
@blur="$emit('blur',$event)"
@focus="$emit('focus',$event)"
>

<!-- @mousedown.native.prevent 不會失去焦點-->
<ac-icon icon="qingkong"
v-if="clearable && value"
@click.native="$emit('input','')"
@mousedown.native.prevent
></ac-icon>
<!-- 先失去 再獲取焦點-->
<ac-icon icon="xianshimima"
v-if="ShowPassword && value"
@click.native="changeState"
></ac-icon>
<ac-icon :icon="suffixIcon"
v-if="suffixIcon"
></ac-icon>
</div>
</template>

<script>
export default {
name: 'ac-input',
data() {
return {
// 儘可能不要直接更改 父組件傳過來的值
password: true
}
},
props: {
type: {
type: String,
default: 'text'
},
name: {
type: String,
default: null
},
placeholder: {
type: String,
default: '請輸入內容'
},
value: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
},
clearable: {
type: Boolean,
default: false
},
ShowPassword: {
type: Boolean,
default: false
},
// 先後icon
prefixIcon: {
type: String
},
suffixIcon: {
type: String
}
},
computed: {
elInputSuffix() {
let classes = []
if (this.clearable || this.ShowPassword || this.suffixIcon) {
classes.push('ac-input-suffix-icon')
}
if (this.prefixIcon) {
classes.push('ac-input-prefix-icon')
}
return classes
}
},
methods: {
changeState() {
this.password = !this.password
this.$nextTick(()=>{
this.$refs.input.focus()
})
}
}
}
</script>

<style lang="scss">
.ac-input {
width: 180px;
display: inline-flex;
position: relative;

input {
border-radius: 4px;
border: 1px solid #dcdfe6;
color: #606266;
height: 40px;
line-height: 40px;
outline: none;
padding: 0 15px;
width: 100%;

&:focus {
outline: none;
border-color: #409eff;
}

&[disabled] {
cursor: not-allowed;
background-color: #f5f7fa;
}
}
}

.ac-input-suffix-icon {
.ac-icon {
position: absolute;
right: 6px;
top: 7px;
cursor: pointer;
}
}

.ac-input-prefix-icon {
input {
padding-left: 30px;
}

.ac-icon {
position: absolute;
left: 8px;
top: 12px;
cursor: pointer;
width: 16px;
height: 16px;
}
}
</style>

先看如下html的代碼結構發現並不難,利用v-if控制 ac-icon的隱藏。利用props傳入屬性來控制。計算屬性控制class的添加

特別注意。記得在組件上寫@xxx="$emit('xxx',$event)"。不然父類觸發不了事件

Upload 上傳組件

html結構

<template>
<div class="ac-upload">
<upLoadDrag v-if="drag" :accpet="accept" @file="uploadFiles">
</upLoadDrag>
<template v-else>
<div @click="handleClick" class="ac-upload-btn">
<slot></slot>
<!-- https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input/file 參考-->
<input class="input" type="file" :accept="accept" :multiple="multiple" :name=name
ref="input" @change="handleChange">
</div>
</template>
<!-- 提示文字-->
<div>
<slot name="tip"></slot>
</div>
<!-- 文件列表 -->
<ul>
<li v-for="(file,index) in files" :key="files.uid">
<div class="list-item">
<ac-icon icon="file"></ac-icon>
{{ file.name }}
<ac-progress v-if="file.status === 'uploading'" :percentage="file.percentage"></ac-progress>
<ac-icon icon="cuowu" @click.native="confirmDel(index)"></ac-icon>
</div>
</li>
</ul>
</div>
</template>

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 整合參數,開始上傳。
<script>
import upLoadDrag from './upLoad-drag'
import ajax from './ajax' // 本身寫的原生ajax

export default {
name: 'ac-upload',
props: {
name: {
type: String,
default: 'file'
},
action: {
type: String,
require: true
},
limit: Number,
fileList: {
type: Array,
default: ()=>[]
},
accept: String,
multiple: Boolean,
onExceed: Function,
onChange: Function,
onSuccess: Function,
onError: Function,
onProgress: Function,
beforeUpload: Function,
httpRequest: { // 提供上傳方法 默認ajax
type: Function,
default: ajax
},
drag: {
type: Boolean,
default: false
}
},
data() {
return {
tempIndex: 0,
files: [],
reqs: {}
}
},
components: {
upLoadDrag
},
watch: { // 監控 當傳入得時候 把用戶原來得文件也放到files裏面 而且格式化
fileList: {
immediate: true,
handler(fileList) {
this.files = fileList.map(item=>{
item.uid = Date.now() + this.tempIndex++
item.status = 'success'
return item
})
}
}
},
methods: {
handleClick() {
console.log(1)
// 點擊前先清空 防止屢次點擊
this.$refs.input.value = ''
this.$refs.input.click()
},
handleChange(e) {
// console.log(e) 從中 target 能夠找到
const files = e.target.files
console.log(files)
this.uploadFiles(files)
},
// 格式化
handleFormat(rawFile) {
rawFile.uid = Math.random() + this.tempIndex++
let file = { // 格式化信息
uid: rawFile.uid, //id
status: 'ready', // 狀態
name: rawFile.name, // 名字
raw: rawFile, // 文件
size: rawFile.size,
percentage: 0 //上傳進度
}
// 把當前用戶 上傳得文件放到列表中 一會要 展現出來
this.files.push(file)
// 接下來 通知文件變化
this.onChange && this.onChange(file)
},
upload(file) {
// 開始上傳
// 若是沒有限制 直接上傳 有限制得話 要進行判斷
if (!this.beforeUpload) {
console.log('上傳')
// 直接上傳
return this.post(file)
}
// 把文件傳給函數進行校驗 獲取結果
let result = this.beforeUpload(file)
console.log(result)
if (result) { // 返回true 纔有意義
// 直接上傳
return this.post(file)
}
},
uploadFiles(files) {
// 判斷上傳個數
if (this.limit && this.fileList.length + files.length > this.limit) {
return this.onExceed && this.onExceed(files, this.fileList)
}
[...files].forEach(file=>{
// 格式化文件 同一文件屢次上傳
this.handleFormat(file)
this.upload(file)
})
},
getFile(rawFile) {
return this.files.find(file=>file.uid === rawFile.uid)
},
handleProgress(ev, rawFile) {
let file = this.getFile(rawFile)
file.status = 'uploading'
file.percentage = ev.percent || 0
this.onProgress(ev, rawFile) // 觸發用戶定義
},
handleSuccess(res, rawFile) {
let file = this.getFile(rawFile)
file.status = 'success'
this.onSuccess(res, rawFile)
this.onChange(file)
},
handleError(err, rawFile) {
let file = this.getFile(rawFile)
file.status = 'fail'
this.onError(err, rawFile)
this.onChange(file)
// 移除文件
delete this.reqs[rawFile.uid]
},
post(file) {
// 上傳邏輯 調用上傳方法
// 整合一下參數 上傳須要傳遞參數
const uid = file.uid
// 配置項
const options = {
file: file,
fileName: this.name, // 傳入得名字
action: this.action,
onProgress: ev=>{
// 處理上傳中得過程
console.log('上傳中', ev)
this.handleProgress(ev, file)
},
onSuccess: res=>{
// 處理上傳成功後
console.log('上傳成功', res)
this.handleSuccess(res, file)
},
onError: err=>{
// 處理上傳失敗後
console.log('上傳失敗', err)
this.handleError(err, file)
}
}
console.log(options)
let req = this.httpRequest(options)
// 把每個ajax 存起來 能夠取消清求
this.reqs[uid] = req //
// 判斷結果 若是返回得是一個promise
if (req && req.then) {
req.then(options.onSuccess, options.onError)
}
},
confirmDel(index){
let res = confirm('確認刪除嗎')
console.log(this.files[index])

if (res){
this.files.pop(index)
}
}
}
}
</script>

<style lang="scss">
.ac-upload {
.ac-upload-btn {
display: inline-block;
}

.input {
display: none;
}
}
</style>

拖拽上傳

相比上面,這裏面就是改了一些 把click 改爲了drop

還有一些文件

<template>
<!-- @drop.prevent="onDrop" 鬆手以後 阻止默認行爲 防止打開文件
@dragover.prevent 劃過
@dragleave.prevent 離開
-->
<div class="ac-upload-drag"
@drop.prevent="onDrag"
@dragover.prevent
@dragleave.prevent
>
<ac-icon icon="shangchuan"></ac-icon>
<span>將文件拖拽到此區域</span>
</div>
</template>

<script>
export default {
name: 'upLoad-drag',
props:{
accept:{
type:String
}
},
methods:{
onDrag(e){
if (!this.accept){
this.$emit('file',e.dataTransfer.files)
}else {
// 本身過濾 過濾以後再次發送
this.$emit('file',e.dataTransfer.files)
}

}
}
}
</script>

<style lang="scss">
.ac-upload-drag{
background-color: #fff;
border: 1px dashed #d9d9d9;
border-radius: 6px;
width: 360px;
height: 180px;
cursor: pointer;
position: relative;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
.ac-icon{
width: 50px;
height: 70px;
}
}
</style>

原生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. 接下來時日曆和時間

最主要可貴就時時間的顯示。得一步一步算。

每一個人的計算方式不同。這裏只給一個參照。

<template>
<div class="ac-date-pick" v-click-outside="handleBlur">
<ac-input suffix-icon="rili" @focus="handleFocus" :value="formatDate" placeholder="請選擇時間"
@change="handleChange"></ac-input>
<!-- content -->
<div class="ac-date-content" v-show="show">
<div class="ac-date-pick-content">
<!-- dates -->
<template v-if="mode === 'dates'">
<div class="ac-date-header">
<ac-icon icon="zuoyi" @click="changeYear(-1)"></ac-icon>
<ac-icon icon="zuo" @click="changeMonth(-1)"></ac-icon>
<span><b @click="mode='years'">{{ TemTime.year }}</b>年 <b @click="mode='months'">{{ TemTime.month+1 }}</b> 月</span>
<ac-icon icon="you" @click="changeMonth(1)"></ac-icon>
<ac-icon icon="youyi1" @click="changeYear(1)"></ac-icon>
</div>
<div>
<span v-for="week in weeks" :key="week" class="week">{{ week }}</span>
</div>
<div v-for="i in 6" :key="`row_${i}`">
<span v-for="j in 7" :key="`col_${j}`" class="week date-hover"
@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()}}
</span>
</div>
</template>
<!-- months -->
<template v-if="mode === 'months'">
<div class="ac-date-header">
<ac-icon icon="zuoyi" @click="changeYear(-1)"></ac-icon>
<span>
<b @click="mode='years'">{{ this.TemTime.year }}</b>年
</span>
<ac-icon icon="youyi1" @click="changeYear(1)"></ac-icon>
</div>
<div>
<div>
<span v-for="(i,index) in month" class="week date-hover year" @click="setMonth(index)">{{ i }}</span>
</div>
</div>
</template>
<!-- years -->
<template v-if="mode === 'years'">
<div class="ac-date-header">
<ac-icon icon="zuoyi" @click="changeYear(-10)"></ac-icon>
<span>
<b @click="mode='years'">{{ startYear() }}</b>年-
<b @click="mode='years'">{{ startYear()+10 }}</b>年
</span>
<ac-icon icon="youyi1" @click="changeYear(10)"></ac-icon>
</div>
<div>
<div>
<span v-for="i in showYears" class="week date-hover year"
@click="setYear(i)"
>{{ i.getFullYear() }}</span>
</div>
</div>
</template>
</div>
</div>
</div>
</template>

<script>
function getTime(date) {
let year = date.getFullYear()
let month = date.getMonth()
let day = date.getDate()
return [year, month, day]
}

import clickOutside from 'v-click-outside'

export default {
name: 'ac-date-pick',
data() {
let [year, month, day] = getTime(this.value || new Date())
return {
show: false,
mode: 'dates',
weeks: ['日', '一', '二', '三', '四', '五', '六'],
month: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
time: { // 負責展現
year, month, day
},
TemTime: { // 臨時時間 修改這個 由於time 是經過父級傳入的值計算出來的 負責修改
year, month, day
}
}
},
watch: {
value(newValue) {
console.log(newValue)
let [year, month, day] = getTime(newValue)
console.log(year, month, day)
this.time = {
year, month, day
}
this.TemTime = { ...this.time }
}
},
computed: {
showDate() {
let firstDay = new Date(this.TemTime.year, this.TemTime.month, this.TemTime.day)
// console.log(firstDay)
let weekDay = firstDay.getDay() // 獲取周幾 0 - 6
// console.log(weekDay)
let day = firstDay.getDate()
// console.log(parseInt((day - weekDay) / 7) + 1)
weekDay = weekDay === 0 ? 7 : weekDay
let start = firstDay - weekDay * 1000 * 60 * 60 * 24 - 7 * (parseInt((day - weekDay) / 7) + 1) * 1000 * 60 * 60 * 24
let arr = []
for (let i = 0; i < 42; i++) {
arr.push(new Date(start + i * 1000 * 60 * 60 * 24))
}
return arr
},
showYears(){
let arr = []
for (let i = 0; i < 10; i++) {
let startYear = new Date(this.startYear(),1)
arr.push(new Date(startYear.setFullYear(startYear.getFullYear() + i)))
}
return arr
},
formatDate() {
if (this.value) {
console.log('這個是爲了確認父級是否傳值。不傳就不渲染input裏面的值')
// padStart padEnd 補全長度的功能。若是某個字符串不夠指定長度,會在頭部或尾部補全
return `${ this.time.year }-${ (this.time.month + 1 + '').padStart(2, 0) }-${ (this.time.day + '').padStart(2, 0) }`
}
}
},
directives: {
clickOutside: clickOutside.directive
},
props: {
value: [String, Date],
default: ()=>new Date()
},
methods: {
handleFocus() { // 控制點擊輸入框彈出浮層
this.show = true
console.log('focus')
},
handleBlur() { // 當點擊 div外側的時候 隱藏浮層
this.show = false
this.mode = 'dates'
console.log('Blur')
},
getCurrentMonth(i, j) {
return this.showDate[(i - 1) * 7 + (j - 1)]
},
getTenYears(i,j){
if (((i - 1) * 4 + (j - 1)) < 10){
return this.showYears[(i - 1) * 4 + (j - 1)]
}
},
isCurrentMonth(date) {
let { year, month } = this.TemTime
let [y, m] = getTime(date)
// console.log(year,month)
// console.log(y,m)
return year === y && month === m
},
isToday(date) {
let [year, month, day] = getTime(date)
let [y, m, d] = getTime(new Date)
return year === y && month === m && day === d
},
selectDay(date) {
this.$emit('input', date)
this.handleBlur()
},
isSelect(date) {
let { year, month, day } = this.time
let [y, m, d] = getTime(date)
return year === y && month === m && day === d
},
changeYear(count) {
let oldDate = new Date(this.TemTime.year, this.TemTime.month)
let newDate = oldDate.setFullYear(oldDate.getFullYear() + count)
let [year] = getTime(new Date(newDate))
this.TemTime.year = year
// this.TemTime.year += mount //這樣改容易有bug
},
changeMonth(count) {
let oldDate = new Date(this.TemTime.year, this.TemTime.month)
let newDate = oldDate.setMonth(oldDate.getMonth() + count)
let [year, month] = getTime(new Date(newDate))
this.TemTime.year = year
this.TemTime.month = month
},
handleChange(e) {
console.log(e.target.value)
let newValue = e.target.value
let regExp = /^(\d{4})-(\d{1,2})-(\d{1,2})$/
if (newValue.match(regExp)) {
// console.log(RegExp.$1,RegExp.$2,RegExp.$3)
this.$emit('input', new Date(RegExp.$1, RegExp.$2 - 1, RegExp.$3))
} else {
e.target.value = this.formatDate
}
},
startYear() {
return this.TemTime.year - this.TemTime.year % 10
},
setYear(date){
this.TemTime.year = date.getFullYear()
this.mode = 'months'
},
setMonth(index){
this.TemTime.month = index
this.mode = 'dates'
}
}
}
</script>

<style lang="scss">
@import "../../styles/var";
@import "../../styles/mixin";

.ac-date-pick {
border: 1px solid red;
display: inline-block;

.ac-date-content {
position: absolute;
z-index: 10;
user-select: none;
width: 280px;
background: #fff;
box-shadow: 1px 1px 2px $primary, -1px -1px 2px $primary;

.ac-date-header {
height: 40px;
@include flexSet()
}

.ac-date-pick-content {
.week {
width: 40px;
height: 40px;
display: inline-block;
text-align: center;
line-height: 40px;
border-radius: 50%;
}
.year{
width: 70px;
height: 70px;
line-height: 70px;
}

.date-hover:hover:not(.isNotCurrentMonth):not(.isSelect) {
color: $primary;
}

.isNotCurrentMonth {
color: #ccc;
}

.isSelect {
background-color: $primary;
color: #fff;
}

.isToday {
background-color: #fff;
color: $primary
}
}
}
}
</style>

Switch 開關組件

switch就相對簡單一點。純樣式控制。input寫到 label內,不須要寫for了。經過僞類控制。

經過computed來控制class樣式添加

<template>
<div class="ac-switch">
<span v-if="activeText" :class="{checkedText:!checked}">{{ activeText }}</span>
<label class="ac-label" :style="labelStyle">
<input type="checkbox" :checked="checked" @click="changCheck" :disabled="disabled">
<span></span>
</label>
<span v-if="inactiveText" :class="{checkedText:checked}">{{ inactiveText }}</span>
</div>
</template>

<script>
export default {
name: 'ac-switch',
props: {
value: {
type: Boolean,
default: false
},
activeText: String,
inactiveText: String,
activeColor:{
type: String,
default:'rgb(19, 206, 102)'
},
inactiveColor: String,
disabled:{
type: Boolean,
default:false
}
},
data() {
return {
checked: this.value
}
},
methods: {
changCheck() {
this.checked = !this.checked
this.$emit('input', this.checked)
}
},
computed:{
labelStyle(){
let style = {}
if (this.checked){
style.backgroundColor = this.activeColor
}else {
style.backgroundColor = this.inactiveColor
}
if (this.disabled){
style.cursor = 'not-allowed'
style.opacity = 0.6
}
return style
}
}
}
</script>

<style lang="scss">
.ac-label {
width: 40px;
height: 20px;
border-radius: 30px;
overflow: hidden;
vertical-align: middle;
position: relative;
display: inline-block;
background: #ccc;
box-shadow: 0 0 1px #36a6d4;

input {
visibility: hidden;
}

span {
position: absolute;
top: 0;
left: 0;
border-radius: 50%;
background: #fff;
width: 50%;
height: 100%;
transition: all linear 0.2s;
}

input:checked + span {
transform: translateX(100%);
}
}
.checkedText {
color: #3a8ee6;
}

</style>

infinteScroll 無限滾動指令

無限滾動不能做爲一個組件。因此放成一個指令。參考地址

  1. attributes 自定義的默認屬性
  2. getScrollContainer  獲取Scroll的容器元素
  3. getScrollOptions  屬性合併
  4. handleScroll 控制是否Scroll

思路。插入的時候 獲取fnvnode.再獲取容器。獲取參數。綁定事件。最後解除綁定

重點說一下  MutationObserver  MDN

import throttle from 'lodash.throttle'
// 自定義屬性
const attributes = {
  delay: {
    default200
  },
  immediate: {
    defaulttrue
  },
  disabled: {
    defaultfalse
  },
  distance: {
    default10
  },

}


/**
 *  獲取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, {
          childListtrue,  // 監控孩子列表發生變化
          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(01);
    for (let i = 0; i < length - 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

這個裏面沒有什麼較難的內容。基本就是樣式的控制

<template>
<transition name="ac-message-fade">
<div v-show="visible"
class="ac-message"
:style="messageStyle"
:class="MesClass"
>
{{ message }}
</div>
</transition>
</template>
<script>
export default {
name: 'Message',
data() {
return {
message: '',
type: '',
visible: false,
duration: 3000,
verticalOffset: 0
}
},
mounted() {
if (this.duration > 0)
setTimeout(()=>{
this.$destroy() // 銷燬當前實例
// 銷燬dom 元素
this.$el.parentNode.removeChild(this.$el)
this.close()
}, this.duration)
},
computed: {
messageStyle() {
let style = {}
style.top = this.verticalOffset + 'px'
style.zIndex = 2000 + this.verticalOffset
return style
},
MesClass() {
const classes = []
if (this.type) {
classes.push(`ac-message-${ this.type }`)
}
return classes
}
}
}
</script>
<style lang="scss">
@import "../../styles/var";

.ac-message {
min-width: 380px;
border-radius: 4px;
border: 1px solid #ebeef5;
position: fixed;
left: 50%;
background-color: #edf2fc;
transform: translateX(-50%);
transition: opacity .3s, transform .4s, top .4s;
overflow: hidden;
padding: 15px 15px 15px 20px;
display: flex;
align-items: center;
@each $type, $color in (success:$success, error:$danger, warning:$warning, info:$info) {
&-#{$type} {
color: $color;
}
}
&-success {
background-color: #f0f9eb;
border-color: #e1f3d8
}
&-warning {
background-color: #fdf6ec;
border-color: #faecd8
}
&-error {
background-color: #fef0f0;
border-color: #fde2e2
}
}

.ac-message-fade-enter, .ac-message-fade-leave-active {
opacity: 0;
transform: translate(-50%, -100%)
}

</style>

Popover 彈出框組件

這個組件跟Message差很少。並不難。主要對JS三你們族的的引用。得到元素位置。根據元素位置來肯定popover的位置

@click.stop阻止事件冒泡

我的以爲這一部分寫的有一點冗餘。感受能夠用offset搞定所有的。可是沒有使用。就先這樣吧

<template>
<div class="ac-popover" ref="parent">
<!-- 阻止事件冒泡-->
<div class="ac-popover-content"
v-show="show"
:class="`popover-${this.placement}`"
:style="position"
ref="content"
@click.stop>
<h3 v-if="title">{{ title }}</h3>
<slot>{{ content }}</slot>
<div class="popover"></div>
</div>
<div ref="reference">
<slot name="reference"></slot>
</div>
</div>
</template>

<script>
const on = (element, event, handler)=>{
element.addEventListener(event, handler, false)
}
const off = (element, event, handler)=>{
element.removeEventListener(event, handler, false)
}
export default {
name: 'ac-popover',
data() {
return {
show: this.value,
clientWidth: 0,
offsetTop: 0,
offsetLeft: 0
}
},
props: {
value: {
type: Boolean,
default: false
},
placement: {
validator(type) {
if (!['top', 'bottom', 'left', 'right'].includes(type)) {
throw new Error('屬性必須是' + ['top', 'bottom', 'left', 'right'].join(','))
}
return true
}
},
width: {
type: [String, Number],
default: '200px'
},
content: {
type: String,
default: ''
},
title: {
type: String,
default: ''
},
trigger: {
type: String,
default: ''
},
},
methods: {
handleShow() {
this.show = !this.show
},
handleDom(e) {
if (this.$el.contains(e.target)) {
return false
}
this.show = false
},
handleMouseEnter() {
clearTimeout(this.time)
this.show = true
},
handleMouseLeave() {
this.time = setTimeout(()=>{
this.show = false
}, 200)
}
},
watch: {
show(value) {
if (value && this.trigger === 'hover') {

this.$nextTick(()=>{
let content = this.$refs.content
document.body.appendChild(content)
on(content, 'mouseenter', this.handleMouseEnter)
on(content, 'mouseleave', this.handleMouseLeave)
})
}
}
},
computed: {
position() {
let style = {}
let width
if (typeof this.width === 'string') {
width = this.width.split('px')[0]
} else {
width = this.width
}
if (this.trigger === 'click') {
if (this.placement === 'bottom' || this.placement === 'top') {
style.transform = `translate(-${ this.clientWidth / 2 }px,-50%)`
style.right = `-${ width / 2 }px`
// console.log(style.right)
} else {
style.top = '-21px'
}
if (this.placement === 'bottom') {
style.top = '-100%'
} else if (this.placement === 'top') {
style.top = '200%'
} else if (this.placement === 'left') {
style.left = '104%'
} else if (this.placement === 'right') {
console.log('click'+this.offsetLeft)
style.left = '-190%'
}
} else if (this.trigger === 'hover') {
if (this.placement === 'bottom' || this.placement === 'top') {
style.left = `${ this.offsetLeft - width / 2 }px`
style.transform = `translateX(${ this.clientWidth / 2 }px)`
} else {
style.top = `${ this.offsetTop - 21 }px`
}
if (this.placement === 'bottom') {
style.top = `${ this.offsetTop - 73 }px`
} else if (this.placement === 'top') {
style.top = `${ this.offsetTop + 49 }px`
} else if (this.placement === 'left') {
console.log(width)
style.left = `${ this.offsetLeft + this.clientWidth + 7 }px`
} else if (this.placement === 'right') {
style.left = `${ this.offsetLeft - width - 6 }px`
}
}
return style
}
},
mounted() {
let reference = this.$slots.reference
console.log(this.$refs.parent.offsetLeft)
this.offsetTop = this.$refs.parent.offsetTop
this.offsetLeft = this.$refs.parent.offsetLeft
this.clientWidth = this.$refs.reference.clientWidth
if (reference) {
// console.log(reference) // 獲取dom節點
this.reference = reference[0].elm
}
if (this.trigger === 'hover') {
on(this.$el, 'mouseenter', this.handleMouseEnter)
on(this.$el, 'mouseleave', this.handleMouseLeave)
} else if (this.trigger === 'click') {
on(this.reference, 'click', this.handleShow)
on(document, 'click', this.handleDom)
}
},
beforeDestroy() {
off(this.$el, 'mouseenter', this.handleMouseEnter)
off(this.$el, 'mouseleave', this.handleMouseLeave)
off(this.reference, 'click', this.handleShow)
off(document, 'click', this.handleDom)
}
}
</script>

<style lang="scss">
.ac-popover {
position: relative;
display: inline-block;
}

.ac-popover-content {
width: 200px;
position: absolute;
padding: 10px;
top: 0;
background-color: #fff;
border-radius: 5px;
box-shadow: -1px -1px 3px #ccc, 1px 1px 3px #ccc;
z-index: 2003;

}

.popover {
position: absolute;

&::after, &::before {
content: '';
display: block;
width: 0;
height: 0;
border: 6px solid #ccc;
position: absolute;
border-left-color: transparent;
border-top-color: transparent;
border-right-color: transparent;
}

&::after {
border-bottom-color: #fff;
/*https://www.runoob.com/cssref/css3-pr-filter.html*/
filter: drop-shadow(0 -2px 1px #ccc);
}
}

.popover-bottom {
.popover {
left: 50%;
margin-left: -6px;
bottom: 0;

&::after, &::before {
transform: rotate(180deg);
}
}
}

.popover-top {
.popover {
left: 50%;
margin-left: -6px;
top: -12px;
}
}

.popover-left {
.popover {
top: 50%;
margin-left: -6px;
left: -6px;

&::after, &::before {
transform: rotate(-90deg);
}
}
}

.popover-right {
.popover {
top: 50%;
margin-left: -6px;
right: 0;

&::after, &::before {
transform: rotate(90deg);
}
}
}
</style>

分頁組件

分頁組件。相比較可貴地方就在。須要計算何時顯示。何時不應顯示。即pagers計算屬性。吧這一部分理解了,基本也就沒什麼了/。主要是一個計算問題

<template>
<ul class="ac-pagination">
<li>
<ac-icon icon="zuo" @click="select(currentPage - 1)" :class="{noAllow: currentPage === 1 }"></ac-icon>
</li>
<li><span :class="{active:currentPage === 1}" @click="select(1)">1</span></li>
<li v-if="showPrev"><span>...</span></li>
<li v-for="p in pagers" :key="p">
<span :class="{active:currentPage === p}" @click="select(p)">
{{p}}
</span>
</li>
<li v-if="showNext"><span>...</span></li>
<li><span :class="{active:currentPage === total}" @click="select(total)">{{ total }}</span></li>
<li>
<ac-icon icon="you" @click="select(currentPage + 1)" :class="{noAllow:currentPage===total}"></ac-icon>
</li>
</ul>
</template>

<script>
export default {
name: 'ac-pagination',
data() {
return {
showPrev: false,
showNext: false
}
},
methods:{
select(current){
if (current <1){
current = 1
}else if (current > this.total){
current = this.total
}else if (current !== this.currentPage){
this.$emit('update:current-page',current)
}
}
},
props: {
total: {
type: Number,
default: 1
},
pageCount: {
type: Number,
default: 7
},
currentPage: {
type: Number,
default: 1
}
},
computed: {
// 最多顯示 7個
// 1 2 3 4 5 6 ...10
// 1 .。3 4 5 6 7 .。。10
pagers() {
// floor向下取整 ceil 向上取整
let middlePage = Math.ceil(this.pageCount / 2)
let showPrev = false
let showNext = false
if (this.total > this.pageCount) {
if (this.currentPage > middlePage) {
showPrev = true
}
if (this.currentPage < this.total - middlePage + 1) {
showNext = true
}
}
let arr = []
if (showPrev && !showNext) {
// 前面存在。。。
let start = this.total - (this.pageCount - 2)
for (let i = start; i < this.total; i++) {
arr.push(i)
}
} else if (showNext && showPrev) {
let count = Math.floor((this.pageCount - 2) / 2)
for (let i = this.currentPage - count; i <= this.currentPage + count; i++) {
arr.push(i)
}
} else if (!showPrev && showNext) {
// 後面存在...
for (let i = 2; i < this.pageCount; i++) {
arr.push(i)
}
} else {
for (let i = 2; i < this.total; i++) {
arr.push(i)
}
}
this.showPrev = showPrev
this.showNext = showNext
return arr
}
}
}
</script>

<style lang="scss">
.ac-pagination {
li {
user-select: none;
list-style: none;
display: inline-flex;
vertical-align: middle;
min-width: 35.5px;
padding: 0 4px;
background: #fff;
.active {
color: #3a8ee6;
}
}

.noAllow{
cursor: not-allowed;
}
}
</style>

table 表格組件

表格做爲一個最經常使用的組件。

着重說一下 固定表頭的作法

  1. 先獲取到表頭的 Dom
  2. 空出一部份距離。再把 thead插入進包裹的地方 便可完成
<template>
<div class="ac-table" ref="wrapper">
<div class="table-wrapper" ref="tableWrapper" :style="{height}">
<table ref="table">
<thead>
<tr>
<th v-for="item in CloneColumn" :key="item.key">
<div v-if="item.type && item.type === 'select'">
<input type="checkbox" :style="{width: item.width + 'px'}" :checked="checkAll" ref="checkAll"
@click="checkAllStatus">
</div>
<span v-else>
{{ item.title }}
<span v-if="item.sortable" @click="sort(item,item.sortType)">
<ac-icon icon="sort"></ac-icon>
</span>
</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row,index) in CloneData" :key="index">
<td v-for="(col,index) in CloneColumn" :key="index">
<div v-if="col.type && col.type === 'select'">
<input type="checkbox" :style="{width: col.width+'px'}" @click="selectOne($event,row)"
:checked="checked(row)">
</div>
<div v-else>
<div v-if="col.slot">
<slot :name="col.slot" :row="row" :col="col"></slot>
</div>
<div v-else>
{{ row[col.key] }}
</div>

</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>

</template>

<script>
export default {
name: 'ac-table',
data() {
return {
CloneColumn: [],
CloneData: [],
checkedList: []
}
},
created() {
this.CloneColumn = [...this.columns]
this.CloneData = [...this.data]
this.CloneData = this.CloneData.map(item=>{
item._id = Math.random()
return item
})
this.CloneColumn = this.CloneColumn.map(item=>{
item.sortType = item.sortType ? item.sortType : 0
this.sort(item, item.sortType)
return item
})
},
props: {
columns: {
type: Array,
default: ()=>[]
},
data: {
type: Array,
default: ()=>[]
},
height: {
type: String
}
},
methods: {
checked(row) {
return this.checkedList.some(item=>item._id === row._id)
},
selectOne(e, selectItem) {
if (e.target.checked) {
this.checkedList.push(selectItem)
} else {
// 沒有標識 須要去除 添加標識
this.checkedList = this.checkedList.filter(item=>item._id !== selectItem._id
)
}
this.$emit('on-select', this.checkedList, selectItem)
},
checkAllStatus(e) {
this.checkedList = e.target.checked ? this.CloneData : []
this.$emit('on-select-all', this.checkedList)
},
sort(col, type) {
let data = [...this.CloneData]
if (type !== 0) {
let key = col.key
if (type === 1) {
data.sort((a, b)=>{
return a[key] - b[key]
})
} else if (type === 2) {
data.sort((a, b)=>{
return b[key] - a[key]
})
}
this.CloneData = data
}
this.$emit('on-list-change', data, col.sortType)
col.sortType = col.sortType === 1 ? 2 : 1
}
},
computed: {
checkAll() {
return this.CloneData.length === this.checkedList.length
}
},
watch: {
checkedList() {
if (this.CloneData.length !== this.checkedList.length) {
if (this.checkedList.length > 0)
return this.$refs.checkAll[0].indeterminate = true
}
this.$refs.checkAll[0].indeterminate = false
}
},
mounted() {
if (this.height) {
let wrapper = this.$refs.wrapper
let tableWrapper = this.$refs.tableWrapper
let table = this.$refs.table

let cloneTable = table.cloneNode()
console.log(cloneTable)
let thead = table.children[0]
console.log(thead.getBoundingClientRect())
tableWrapper.style.paddingTop = thead.getBoundingClientRect().height + 'px'

cloneTable.style.width = table.offsetWidth + 'px'
cloneTable.appendChild(thead)
cloneTable.classList.add('fix-header')

// 設置對其 querySelector獲取文檔種DOM元素
let tds = table.querySelector('tbody tr').children
console.log(tds)
let ths = cloneTable.querySelector('thead tr').children
tds.forEach((item, index)=>{
ths[index].style.width = item.getBoundingClientRect().width + 'px'
})
wrapper.appendChild(cloneTable)
}
}
}
</script>

<style lang="scss">
.ac-table {
position: relative;
overflow: hidden;

.fix-header {
position: absolute;
top: 0;
}

.table-wrapper {
overflow-y: scroll;
}

table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;

thead {
th {
background-color: #f8f8f9;
white-space: nowrap;
}
}

tbody {
tr:hover {
background-color: #7dbcfc;
}
}

th, td {
border-bottom: 1px solid #ddd;
padding: 10px;
text-align: left;
}
}
}
</style>

Vuepress 配置

有關Vuepress不作過多的解釋了。官網 直接進入正題

貼一下本身的


導航欄配置

官方文檔

總結

本篇文章介紹了部分組件的我的開發過程。學習到的

  1. 關於 sass語法的使用。
  2. 還有就是組件設計時考慮的全面與否
  3. 一些組件的設計遇到的麻煩。再閱讀源碼以後解決。必定的獨立思考和解決能力
  4. 不一樣組件的寫法。
  5. Vuepress的配置

抒發迷茫

  1. 常常被打擊。不知道將來到底須要做什麼。
  2. 身爲一個前端工程師。沒有什麼拿得出手的做品。
  3. 東西突飛猛進。有好多東西本身還有去學。小程序, flutter等。感受到有點累
  4. 優化策略  沒接觸或者實際操做過。
  5. 想去接觸一下實際的工做。不想再去模仿。
  6. 繼續加油吧


本文分享自微信公衆號 - 阿琛前端成長之路(lwkWyc)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索