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.
- Server cause.
- Node services are not packaged to start. Pack because used
Vue components
, so an error occurs. I’m not going to solve it yet. There’s something you can do to help solve it. - Attach a screenshot
Github address here
A total of 12 components have been written, large and small. , respectively,
- The Button component
- Layout Components
- Container Container components
- Input Input box component
- Upload Upload component
- DatePick calendar component
- Switch Switch component
- InfinteScroll wireless scroll command
- Message notification component
- Popover Popover component
- The paging component
- 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
type
Type, respectively control button different coloricon
Font icon. See if the button should have an iconiconPosition
Position of font icon.loading
Loading statusdisable
和loading
Control together- The following is not implemented, feeling relatively simple. So be lazy
size
Button size (here I am lazy, feel this is easier to implement)radio
Rounded 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
- I am using
order
To carry out the icon position before and after, also can againspan
I’m going to add another oneac-icon
withif
Judgment can be @click
The event is needed to trigger the parentclick
Events. 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
validator
, custom validatorReference documentationcomputed
Dynamic binding based on incoming propertiesclass
There 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.
- a
row
On behalf of the line - a
col
On 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
mounted
. insideGet all the children, let’s do thatgutter
Assigned to them. style
Why deconstruct it in case there’s a pattern in it- 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
- But it
- Password to show
- Input box with icon
- 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
- According to the incoming
drag
To determine whether drag-and-drop upload is required - File list. Depending on the state, the display is determined
progress
Js, CSS structure
CSS is just a few lines. So I’m just going to write it right here
Props to explain
name
The name of the input box submitted to the backgroundaction
Submit the address:limit
Limit the number of submissionsaccept
type:on-exceed
If the number of commits exceeds, the method is executed twice:on-change
When the status of the uploaded file changes, the selected file is uploaded successfully:on-success
Triggered when the upload succeeds:on-error
Triggered when uploading failed:on-progress
Triggered during upload:before-upload
Function triggered before uploading:file-list
Upload File ListhttpRequest
Provide an upload method, for exampleaixos
The defaultajax
JS this long string of code can be a headache to look at. So let me go through the process.
- First turn on the
input
Hide it. Click on thediv
. The triggerhandleClick
Method to empty the value, andclick
input
- Triggered when a file is selected
change
handleChange
Events. Get the list of files and start preparing for upload uploadFiles
Method, get the number of files, passhandleFormat
Format the file and passupload
uploadupload
Determine if there isbeforeUpload
Pass it in, pass it in, and upload it if you don’tpost
Consolidate 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
input
After focusing, executehandleFocus
Function to display the calendar box below. Click on thediv
Outside. performhandleBlur
. Shut downCalendar box
- The following is
content
The inside of. Show the head, 4icon
Additional time display - 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
attributes
Custom default propertiesgetScrollContainer
Gets the container element of ScrollgetScrollOptions
Attribute to mergehandleScroll
Control 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
- through
extend
Method to generate avue
Subclasses. Then through$mount
generateDom object
To add to thedocument
options.close
inelement
It’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
- Because there could be more than one
message
. 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
- Get the header first
Dom
- To clear some distance. Then put the
thead
Insert 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
- about
sass
Use of grammar. - There is also the component design consideration of the comprehensive
- Some component design problems. Then read the source code to solve. Independent thinking and solving ability
- Writing of different components.
Vuepress
The configuration of the
Express confusion
- Often hit. I don’t know what I need to do.
- As a front-end engineer. There was nothing to show for it.
- Things are changing fast. There are a lot of things I still have to learn. Small program,
flutter
And so on. Feeling a little tired - The optimization strategy has not been touched or practiced.
- I want to get my hands on real work. I don’t want to imitate anymore.
- Keep it up