preface

This article is to exercise their technical ability and foundation, imitating element- UI to develop the UI library. Purely for learning to use. This paper uses VUE-CLI4 to construct. Sass for CSS precompilers

Document address here

You might die. NPM run docs:dev cannot log in once closed.

A little bit about documentation. It’ll be a little slow to get in.

  1. Server cause.
  2. Node services are not packaged to start. Pack because usedVue components, so an error occurs. I’m not going to solve it yet. There’s something you can do to help solve it.
  3. Attach a screenshot

Github address here

A total of 12 components have been written, large and small. , respectively,

  1. The Button component
  2. Layout Components
  3. Container Container components
  4. Input Input box component
  5. Upload Upload component
  6. DatePick calendar component
  7. Switch Switch component
  8. InfinteScroll wireless scroll command
  9. Message notification component
  10. Popover Popover component
  11. The paging component
  12. Table component

That’s about it. Without further ado, let’s start parsing and creating each component

The code structure

ui

|-- undefined |-- .browserslistrc |-- .editorconfig |-- .eslintrc.js |-- .gitignore |-- babel.config.js |-- Karma. Conf. Js/configuration/karma | -- package - lock. Json | -- package. Json | - packeage explanation. TXT | -- README. Md | -- services. Js / / File upload server | - vue. Config. Js | - dist / / packaging after | | - ac-ui.com mon. Js | | - ac-ui.com mon. 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 | | - the favicon. Ico | | -- index. | - HTML/SRC/home folder | | - App. Vue | | -- main. Js | | - assets | | | -- logo. PNG | | - components / / test cases | | | -- 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 | | - global style styles / / | | -- icon. Js | | - Mixins. SCSS | | - _var. SCSS | | - tests / / test cases | -- button. Spec. Js | | - col. Spec. Js | | - uploads / / file upload path - 1. JsCopy the code

Generic code

style

// 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;
}

Copy the code

With the function

// Flex layout reuse
@import "var";
@mixin flexSet($dis:flex,$hov:space-between,$ver:middle,$col:center) {
  display: $dis;
  justify-content: $hov; // Spindle alignment
  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 cannot convert to pure CSS. Using a variable or parameter value as a CSS function results in an error. Use the inspect($value) function to generate an output string useful for debugging maps.
  @media only screen and #{inspect(map_get($map,$key))}{
    @content// slot}}Copy the code

The Button component

Button

The first thing to confirm is what common attributes a Button has

  1. typeType, respectively control button different color
  2. iconFont icon. See if the button should have an icon
  3. iconPositionPosition of font icon.
  4. loadingLoading status
  5. disableloadingControl together
  6. The following is not implemented, feeling relatively simple. So be lazy
  7. sizeButton size (here I am lazy, feel this is easier to implement)
  8. radioRounded corners are just adding oneborder-radius

That’s all I can think of for now. To achieve the goal of

HTML structure

<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>
Copy the code

This code should be easy to understand. Pay attention to the point

  1. I am usingorderTo carry out the icon position before and after, also can againspanI’m going to add another oneac-iconwithifJudgment can be
  2. @clickThe event is needed to trigger the parentclickEvents. You can add more if you need more

JS part

<script>
  export default {
    name: 'ac-button'.props: {
      type: {
        type: String.default: ' '.validator(type) {
          if(type && ! ['waring'.'success'.'danger'.'info'.'primary'].includes(type)) {
            console.error('Type Type must be' + ['waring'.'success'.'danger'.'info'.'primary'].join(', '))}return true}},icon: {
        type: String
      },
      iconPosition: {
        type: String.default: 'left'.validator(type) {
          if(type && ! ['left'.'right'].includes(type)) {
            console.error('Type Type must be' + ['left'.'right'].join(', '))}return true}},loading: {
        type: Boolean.default: false}},computed: {
      btnClass() {
        const classes = []
        if (this.type) {
          classes.push(`ac-button-The ${this.type }`)}if (this.iconPosition) {
          classes.push(`ac-button-The ${this.iconPosition }`)}return classes
      }
    }
  }
</script>
Copy the code

Js part of this is easy to understand. Mainly explain the following parts

  1. validator, custom validatorReference documentation
  2. computedDynamic binding based on incoming propertiesclassThere are several ways to do this here is just one of them

The CSS part

<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; // Whether text can be selected
    &: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>
Copy the code

The CSS button style is relatively simple.

Mention the sass@each usage. Reference documentation

It’s a loop that loops through arrays or objects, similar to Python’s for loop

Icon

This one is a little bit easier and I’ll just go to the code

<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>Copy the code

ButtonGroup

This is a little bit easier. It’s just filling in the slot. Then change the style.

You can also write an error message

<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',' child must be 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>Copy the code

Layout Components

Refer to element-UI, which has two components.

  1. arowOn behalf of the line
  2. acolOn behalf of the column

Analyze the function of the lines, control the arrangement of elements, the direct distance of elements, etc., and then reveal the contents

Columns need to control their size, offset. The response etc.

Let’s start implementing it.

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 must be '+ ['start', 'end', 'content', 'space-around', 'space-between']. '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>Copy the code

HTML is simple; it renders what comes in. The props aspect is also relatively simple, with a custom validator. I said that before. Explain the others

  1. mounted. insideGet all the children, let’s do thatgutterAssigned to them
  2. . styleWhy deconstruct it in case there’s a pattern in it
  3. The Flex layout is used directly here. Have energy small partners can be added to float

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 "> /* Create width sass syntax with loop 24 */ @import "./.. /.. /styles/_var"; /* % layout */ @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%; }} / * * / responsive layout @ 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>Copy the code

The core of this code is to add different classes to the component by evaluating properties

About the res below and in the generic code above. Just some applications of SASS

Container Container components

Container components are relatively simple. Is to take advantage of the new H5 label.

It uses 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>Copy the code

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>


Copy the code

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>


Copy the code

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>


Copy the code

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>

Copy the code

Input Input box component

Referring to Element, it should do the following

  1. But it
  2. Password to show
  3. Input box with icon
  4. Disabled state
<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>


Copy the code

First look at the following HTML code structure found not difficult, using V-if control ac-icon hidden. Control using the props passed property. Compute properties control the addition of classes

Pay special attention. Remember to write @xxx=”$emit(‘ XXX ‘,$event)” on the component. Otherwise, the parent class cannot fire the event

Upload Upload component

HTML structure

<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> <! Reference - 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> <! - prompt words -- -- > < div > < slot name = "tip" > < / slot > < / div > <! -- File list --> <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>Copy the code

UpLoadDrag is going to drag and upload

Reference when type = file

Explain the HTML structure

  1. According to the incomingdragTo determine whether drag-and-drop upload is required
  2. File list. Depending on the state, the display is determinedprogress

Js, CSS structure

CSS is just a few lines. So I’m just going to write it right here

Props to explain

  1. nameThe name of the input box submitted to the background
  2. actionSubmit the address
  3. :limitLimit the number of submissions
  4. accepttype
  5. :on-exceedIf the number of commits exceeds, the method is executed twice
  6. :on-changeWhen the status of the uploaded file changes, the selected file is uploaded successfully
  7. :on-successTriggered when the upload succeeds
  8. :on-errorTriggered when uploading failed
  9. :on-progressTriggered during upload
  10. :before-uploadFunction triggered before uploading
  11. :file-listUpload File List
  12. httpRequestProvide an upload method, for exampleaixosThe defaultajax

JS this long string of code can be a headache to look at. So let me go through the process.

  1. First turn on theinputHide it. Click on thediv. The triggerhandleClickMethod to empty the value, andclick input
  2. Triggered when a file is selectedchange handleChangeEvents. Get the list of files and start preparing for upload
  3. uploadFilesMethod, get the number of files, passhandleFormatFormat the file and passuploadupload
  4. uploadDetermine if there isbeforeUploadPass it in, pass it in, and upload it if you don’t
  5. postConsolidate parameters and start uploading.
<script> import upLoadDrag from './ upload-drag 'import ajax from './ajax' '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: {// provide default ajax type: Function, default: ajax}, drag: {type: Boolean, default: false } }, data() { return { tempIndex: 0, files: [], reqs: {} } }, components: { upLoadDrag }, watch: {// Monitor the user's original files in the files when passed in and format fileList: {immediate: true, handler(fileList) { this.files = fileList.map(item=>{ item.uid = Date.now() + this.tempIndex++ item.status = 'success' return item }) } } }, methods: $refs.input.value = "this.$refs.input.click()}", {handleClick() {console.log(1); HandleChange (e) {// console.log(e) const files = e.targe.files console.log(files) this.uploadFiles(files) }, HandleFormat (rawFile) {rawfile.uid = math.random () + this.tempindex+ + let file = {handleFormat(rawFile) {rawfile.uid = math.random () + this.tempindex+ + let file = { Uid, // ID status: 'ready', // Status name: rawfile. name, // Name raw: rawFile, // File size: rawfile. size, percentage: This.files.push (file) this.onchange && this.onchange (file)}, Upload (file) {// start upload // if there is no limit to direct upload if there is a limit to judge if (! This.beforeupload) {console.log(' upload ') // Upload directly return this.post(file)} // Pass the file to the function for verification to get the result let result = This.beforeupload (file) console.log(result) if (result) {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=>{// format file this.filelist (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) // Trigger user definition}, 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) {// upload logic calls upload method // integrate parameters const uid = file.uid // configuration item const options = {file: file, fileName: This. action: this.action, onProgress: Ev =>{console.log(' upload ', ev) this.handleProgress(ev, file)}, onSuccess: Res =>{console.log(' uploads successfully ', res) this.handleSuccess(res, file)}, onError: Console. log(' upload failed ', err) this.handleError(err, File)}} console.log(options) let req = this.httprequest (options) // Save each ajax can be uncleared this.reqs[uid] = req // // If (req&&req.then) {req.then(options.onsuccess, options.onerror)}}, ConfirmDel (index){let res = confirm(' confirm delete? ') 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>Copy the code

Drag and drop to upload

This is a bit of a change from click to drop

And some of the files

<template> <! <div class="ac-upload-drag" --> <div class="ac-upload-drag" --> <div class="ac-upload-drag" @drop.prevent="onDrag" @dragover.prevent @dragleave.prevent > <ac-icon icon="shangchuan"></ac-icon> <span> Drag the file to this area </span> </div> </template> <script> export default {name: 'upLoad-drag', props:{ accept:{ type:String } }, methods:{ onDrag(e){ if (! $emit('file', e.datatransfer.files)}else {// Emit 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>Copy the code

Native ajax

export default function ajax(options) {
  // Create an object
  const xhr = new XMLHttpRequest()
  const action = options.action

  const fd = new FormData() // H5 upload file API
  fd.append(options.fileName,options.file)
  // console.log(options.fileName,options.file)

  / / the console. The log (' file name '+ options. The fileName, the options. The file)

  xhr.onerror = function (err){
    options.onError(err) // Trigger an error callback
  }

  // Use this method H5 API after uploading
  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)
  }

  // Start clearing
  xhr.open('post',action,true)

  // Send clearances
  xhr.send(fd)
  return xhr
}

Copy the code

DatePick calendar component

The structure of the calendar component is not difficult. The rare thing is to count the hours

Let me explain

  1. inputAfter focusing, executehandleFocusFunction to display the calendar box below. Click on thedivOutside. performhandleBlur. Shut downCalendar box
  2. The following iscontentThe inside of. Show the head, 4iconAdditional time display
  3. Next is the calendar and time

The most important thing is the display of time. You have to do it step by step.

Everyone counts differently. Here is only one reference.

<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>

Copy the code

Switch Switch component

Switch is relatively simple. Pure style control. Input goes inside the label, not for. Controlled by pseudo-classes.

Control class style addition through computed

<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 { 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%; The transition: all linear 0.2 s; } input:checked + span { transform: translateX(100%); } } .checkedText { color: #3a8ee6; } </style>Copy the code

InfinteScroll infinite scroll instruction

Infinite scrolling cannot be used as a component. So put it in an instruction. Refer to the address

  1. attributesCustom default properties
  2. getScrollContainer Gets the container element of Scroll
  3. getScrollOptionsAttribute to merge
  4. handleScrollControl Scroll

Train of thought. Get fn and vNode at insert time. Then get container. Get parameters. Bind events. Finally unbind

Focus on MutationObserver MDN

import throttle from 'lodash.throttle'
// Custom attributes
const attributes = {
  delay: {
    default: 200
  },
  immediate: {
    default: true
  },
  disabled: {
    default: false
  },
  distance: {
    default: 10}},/** * get the Scroll container element *@param El element node *@returns {(() => (Node | null))|ActiveX.IXMLDOMNode|(Node & ParentNode)|Window}* /
const getScrollContainer = (el) = >{
  let parent = el
  while (parent) {
    if (document.documentElement === parent) {
      return window
    }
    // Gets whether the element has overflow attributes
    const overflow = getComputedStyle(parent)['overflow-y']
    if (overflow.match(/scroll|auto/)) {
      return parent
    }
    parent = parent.parentNode
  }
}

/** * take the passed attributes and compare them with the default attributes@param El node *@param Vm Vue instance *@returns {{}} The merged property */
const getScrollOptions = (el, vm) = >{
  / / entries reference 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'] // bind 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){ // Contact monitoring
      observer.disconnect()
      this['infinite-scroll'].observer = null}}}export default {
  name: 'infinite-scroll'.inserted(el, bindings, vNode) { // vNode has a context to access the context
    // Insert instruction takes effect
    console.log('Order in effect')
    console.log(bindings.value) // Get fn
    console.log(vNode.context) // Get attributes in the virtual instance
    let cb = bindings.value
    let vm = vNode.context
    // 1. Start looking for the container for the loop
    let container = getScrollContainer(el)
    console.log(container)
    if(container ! = =window) {
      console.log('Bind event')
      // 2. Obtain Options
      let { delay, immediate } = getScrollOptions(el, vm)
      // 3. Perform function throttling to add rolling events
      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)  // Check whether the page continues to load
        observe.observe(container, {
          childList: true.// Monitor child list changes
          subtree: true  // Also emitted when the child DOM element changes
        })
        onScroll() // Load first by default
      }

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

  unbind(el) {
    / / remove
    const { container, onScroll } = el
    if (container) {
      container.removeEventListener('scroll', onScroll)
      el['infinite-scroll'] = {}}}}Copy the code

Message notification component

There are two of them. Why two more because message is added to the Dom via appendChild

Train of thought

  1. throughextendMethod to generate avueSubclasses. Then through$mountgenerateDom objectTo add to thedocument
  2. options.closeinelementIt’s not written that way in the method and it’s part judgment and so on. Here is directly lazy, can be used normally

index

  1. Because there could be more than onemessage. You need to calculate the height. So we use arrays. Cycle height according to number
import Vue from 'vue'
import MessageCom from './Message.vue';

let instances = []
// Generate a vue subclass
let MessageConstructor = Vue.extend(MessageCom)

// Some modifications and simplifications have been made to the writing of element
const Message = (options) = >{
  options.close = function() {
    let length = instances.length
    instances.splice(0.1);
    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
}

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


export default Message

Copy the code

message

There’s nothing hard about this. It’s basically style control

<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() 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>Copy the code

Popover Popover component

This component is similar to Message. Is not difficult. The main reference to JS three families. Gets the element position. Position popovers based on element positions

@click.stop prevents events from bubbling

Personally, I think this part is a little redundant. I feel like I can do all of that with offset. But it was not used. I’ll leave it at that

<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 'and' bottom 'and' left 'and' right ']. Includes (type)) {throw new Error (' attribute must be '+ [' 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) / / for this. The dom node 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>Copy the code

The paging component

Paging component. Here’s where it’s harder. You need to calculate when to display it. When not to display. That is, Pagers computes attributes. Now that you understand this part, it’s basically nothing /. It’s mostly a calculation problem

<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: {// Display a maximum of 7 entries // 1 2 3 4 5 6... 1. 3 4 5 6 7. 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) {// There is a... 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) {// there is... 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.5 px; padding: 0 4px; background: #fff; .active { color: #3a8ee6; } } .noAllow{ cursor: not-allowed; } } </style>Copy the code

Table component

Tables as one of the most commonly used components.

Focus on fixing the head of the table

  1. Get the header firstDom
  2. To clear some distance. Then put thetheadInsert it into the package
<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 {// No identifiers needed to remove add identifiers 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') // Set the DOM element for its querySelector 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>Copy the code

Vuepress configuration

I won’t explain too much about Vuepress. The official website gets straight to the point

Post your own

Navigation Bar Configuration

The official documentation

conclusion

This article describes the personal development process for some of the components. Learn to

  1. aboutsassUse of grammar.
  2. There is also the component design consideration of the comprehensive
  3. Some component design problems. Then read the source code to solve. Independent thinking and solving ability
  4. Writing of different components.
  5. VuepressThe configuration of the

Express confusion

  1. Often hit. I don’t know what I need to do.
  2. As a front-end engineer. There was nothing to show for it.
  3. Things are changing fast. There are a lot of things I still have to learn. Small program,flutterAnd so on. Feeling a little tired
  4. The optimization strategy has not been touched or practiced.
  5. I want to get my hands on real work. I don’t want to imitate anymore.
  6. Keep it up