從0開始,手把手帶你打造本身的UI庫(附文檔)
前言
本篇文章是爲了鍛鍊本身的技術能力還有底子,模仿element-ui
進行開發的UI
庫。純屬學習使用。本文利用Vue-Cli4
進行構建。css預編譯器
用的sass
css
文檔地址這裏html
說一下文檔。進去會有點慢。前端
-
服務器緣由。 -
沒有打包 啓動的node服務。(打包由於使用了 vue組件
,因此出現錯誤。目前我還不會解決。有大能能夠幫忙解決一下最好)
github地址 這裏vue
本次大大小小總共寫了 12 組件。分別是node
-
Button組件 -
Layout 佈局組件 -
Container 容器組件 -
input 輸入框組件 -
Upload 上傳組件 -
DatePick 日曆組件 -
Switch 開關組件 -
infinteScroll 無線滾動指令 -
Message 通知組件 -
Popover 彈出框組件 -
分頁組件 -
table 表格組件
大概就這麼多。廢話很少說,接下來開始進行每一個組件的解析和建立python
代碼結構
ui
|-- undefined
|-- .browserslistrc
|-- .editorconfig
|-- .eslintrc.js
|-- .gitignore
|-- babel.config.js
|-- karma.conf.js //karma 配置
|-- package-lock.json
|-- package.json
|-- packeage解釋.txt
|-- README.md
|-- services.js // 文件上傳服務器
|-- vue.config.js
|-- dist // 打包後
| |-- ac-ui.common.js
| |-- ac-ui.common.js.map
| |-- ac-ui.css
| |-- ac-ui.umd.js
| |-- ac-ui.umd.js.map
| |-- ac-ui.umd.min.js
| |-- ac-ui.umd.min.js.map
| |-- demo.html
|-- public
| |-- 1.html
| |-- favicon.ico
| |-- index.html
|-- src // 主文件夾
| |-- App.vue
| |-- main.js
| |-- assets
| | |-- logo.png
| |-- components // 測試用例
| | |-- ButtonTest.vue
| | |-- ContainerTest.vue
| | |-- DatePickTest.vue
| | |-- FormTest.vue
| | |-- InfiniteScrollTest.vue
| | |-- LayoutTest.vue
| | |-- MessageTest.vue
| | |-- paginationTest.vue
| | |-- PopoverTest.vue
| | |-- SwitchTest.vue
| | |-- TableTest.vue
| |-- packages // UI
| | |-- index.js
| | |-- infiniteScroll.js
| | |-- progress.vue
| | |-- button
| | | |-- Button.vue
| | | |-- ButtonGroup.vue
| | | |-- Icon.vue
| | |-- container
| | | |-- aside.vue
| | | |-- container.vue
| | | |-- footer.vue
| | | |-- header.vue
| | | |-- main.vue
| | |-- datePack
| | | |-- date-pick.vue
| | | |-- date-range-pick.vue
| | |-- Form
| | | |-- ajax.js
| | | |-- input.vue
| | | |-- upLoad-drag.vue
| | | |-- upLoad.vue
| | |-- layout
| | | |-- Col.vue
| | | |-- Row.vue
| | |-- Message
| | | |-- index.js
| | | |-- Message.vue
| | |-- pagination
| | | |-- pagination.vue
| | |-- popover
| | | |-- popover.vue
| | |-- switch
| | | |-- Switch.vue
| | |-- Table
| | |-- Table.vue
| |-- styles // 全局樣式
| |-- icon.js
| |-- mixin.scss
| |-- _var.scss
|-- tests // 測試用例
| |-- button.spec.js
| |-- col.spec.js
|-- uploads // 文件上傳路徑
|-- 1.js
通用代碼
樣式
// styles/_var
$border-radius: 4px;
$primary: #409EFF;
$success: #67C23A;
$warning: #E6A23C;
$danger: #F56C6C;
$info: #909399;
$primary-hover: #66b1ff;
$success-hover: #85ce61;
$warning-hover: #ebb563;
$danger-hover: #f78989;
$info-hover: #a6a9ad;
$primary-active: #3a8ee6;
$success-active: #5daf34;
$warning-active: #cf9236;
$danger-active: #dd6161;
$info-active: #82848a;
$primary-disabled: #a0cfff;
$success-disabled: #b3e19d;
$warning-disabled: #f3d19e;
$danger-disabled: #fab6b6;
$info-disabled: #c8c9cc;
$--xs: 767px !default;
$--sm: 768px !default;
$--md: 992px !default;
$--lg: 1200px !default;
$--xl: 1920px !default;
$map: (
"xs":(max-width:$--xs),
"sm":(min-width:$--sm),
"md":(min-width:$--md),
"lg":(min-width:$--lg),
"xl":(min-width:$--xl),
);
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
混入函數
//flex佈局複用
@import "var";
@mixin flexSet($dis:flex,$hov:space-between,$ver:middle,$col:center) {
display: $dis;
justify-content: $hov; // 主軸對齊方式
align-items: $col;
vertical-align: $ver // 圖片對其
};
@mixin position($pos:absolute,$top:0,$left:0,$width:100%,$height:100%){
position: $pos;
top: $top;
left: $left;
width: $width;
height: $height;
};
@mixin res($key) {
// inspect Map 沒法轉換爲純 CSS。使用一個做爲 CSS 函數的變量或參數的值將致使錯誤。使用inspect($value)函數來生成一個對調試 map 有用的輸出字符串。
@media only screen and #{inspect(map_get($map,$key))}{
@content //插槽
}
}
Button 組件
Button
首先要確認的是,Button
都有哪些經常使用的屬性css3
-
type
類型,分別控制按鈕不一樣的顏色 -
icon
字體圖標。看按鈕是否要帶有圖標 -
iconPosition
字體圖標的位置。 -
loading
加載狀態 -
disable
和loading
一塊兒控制 -
如下沒有實現,感受比較簡單。因此偷個懶 -
size
按鈕大小 (這裏我就偷懶了,感受這個比較好實現) -
radio
圓角 也就是加一個border-radius
暫時就想到這麼多。先實現把git
html結構
<template>
<button class="ac-button" :class="btnClass" :disabled="loading" @click="$emit('click',$event)">
<ac-icon v-if="icon && !loading" :icon="icon" class="icon"></ac-icon>
<ac-icon v-if="loading" icon="xingzhuang" class="icon"></ac-icon>
<span v-if="this.$slots.default">
<slot></slot>
</span>
</button>
</template>
這段代碼應該比較容易理解。注意點github
-
我是利用 order
來進行 圖標位置的先後,也能夠再span
後面在加上一個ac-icon
用if
判斷便可 -
@click
事件是須要觸發 父級click
事件。若是有其餘須要還能夠繼續添加
JS部分
<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類型必須是' + ['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類型必須是' + ['left', 'right'].join(','))
}
return true
}
},
loading: {
type: Boolean,
default: false
}
},
computed: {
btnClass() {
const classes = []
if (this.type) {
classes.push(`ac-button-${ this.type }`)
}
if (this.iconPosition) {
classes.push(`ac-button-${ this.iconPosition }`)
}
return classes
}
}
}
</script>
js
部分這裏面也好理解。主要解釋一下如下部分web
-
validator
,自定義校驗器 參考文檔 -
computed
根據傳入屬性動態綁定class
,有好幾種方法,這裏只是其中一種
css部分
<style lang="scss">
@import "../../styles/var";
@import "../../styles/mixin";
$height: 42px;
$font-size: 16px;
$color: #606266;
$border-color: #dcdfe6;
$background: #ecf5ff;
$active-color: #3a8ee6;
.ac-button {
border-radius: $border-radius;
border: 1px solid $border-color;
height: $height;
color: $color;
font-size: $font-size;
line-height: 1;
cursor: pointer;
padding: 12px 20px;
@include flexSet($dis: inline-flex, $hov: center);
user-select: none; // 是否能夠選中文字
&:hover, &:focus {
color: $primary;
border-color: $border-color;
background-color: $background;
}
&:focus {
outline: none;
}
&:active {
color: $primary-active;
border-color: $primary-active;
background-color: $background;
}
@each $type, $color in (primary:$primary, success:$success, danger:$danger, waring:$warning, info:$info) {
&-#{$type} {
background-color: $color;
border: 1px solid $color;
color: #fff;
}
}
@each $type, $color in (primary:$primary-hover, success:$success-hover, danger:$danger-hover, waring:$warning-hover, info:$info-hover) {
&-#{$type}:hover, &-#{$type}:focus {
background-color: $color;
border: 1px solid $color;
color: #fff;
}
}
@each $type, $color in (primary:$primary-active, success:$success-active, danger:$danger-active, waring:$warning-active, info:$info-active) {
&-#{$type}:active {
background-color: $color;
border: 1px solid $color;
color: #fff;
}
}
@each $type, $color in (primary:$primary-disabled, success:$success-disabled, danger:$danger-disabled, waring:$warning-disabled, info:$info-disabled) {
&-#{$type}[disabled] {
cursor: not-allowed;
color: #fff;
background-color: $color;
border-color: $color;
}
}
.icon {
width: 16px;
height: 16px;
}
&-left {
svg {
order: 1
}
span {
order: 2;
margin-left: 4px;
}
}
&-right {
svg {
order: 2
}
span {
order: 1;
margin-right: 4px;
}
}
}
</style>
css
的button
樣式相對比較簡單。
提一下Sass @each
用法。參考文檔
就是一個循環,能夠循環 數組 或者 對象,相似 python
的for
循環
Icon
這個也比較簡單 就直接上代碼了
<template>
<svg class="ac-icon" aria-hidden="true" @click="$emit('click')">
<use :xlink:href="`#icon-${icon}`"></use>
</svg>
</template>
<script>
import '../../styles/icon.js'
export default {
name: 'ac-icon',
props:{
icon:{
type: String,
require: true
}
}
}
</script>
<style lang="scss">
.ac-icon {
width: 25px;
height:25px;
vertical-align: middle;
fill: currentColor;
}
</style>
ButtonGroup
這個就比較簡單了。就是利用插槽
,內容填充一下。而後更改一下樣式便可。
固然 也能夠寫一個報錯信息
<template>
<div class="ac-button-group">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'ac-button-group',
mounted() {
let children = this.$el.children
for (let i = 0; i < children.length; i++) {
console.assert(children[i].tagName === 'BUTTON','子元素必須是button')
}
}
}
</script>
<style scoped lang="scss">
@import "../../styles/mixin";
@import "../../styles/var";
.ac-button-group{
@include flexSet($dis:inline-flex);
button{
border-radius: 0;
&:first-child{
border-top-left-radius: $border-radius;
border-bottom-left-radius: $border-radius;
}
&:last-child{
border-top-right-radius: $border-radius;
border-bottom-right-radius: $border-radius;
}
&:not(first-child){
border-left: none;
}
}
}
</style>
Layout 佈局組件
參考element-ui
,有兩個組件。
-
一個 row
表明行 -
一個 col
表明列
分析一下行的做用,控制元素的 排列方式,元素直接的距離等,再把裏面內容展示出來
列的做用 須要控制本身所佔大小,偏移。響應等
接下來開始實現。
row
<template>
<div class="ac-row" :style="rowStyle">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'ac-row',
props:{
gutter:{
type:Number,
default:0
},
justify:{
type: String,
validator(type){
if (type && !['start', 'end', 'content', 'space-around', 'space-between'].includes(type)) {
console.error('type類型必須是' + ['start', 'end', 'content', 'space-around', 'space-between'].join(','))
}
return true
}
}
},
mounted() {
this.$children.forEach(child=>{
child.gutter = this.gutter
})
},
computed:{
rowStyle(){
let style={}
if (this.gutter){
style = {
...style,
marginLeft: -this.gutter/2 + 'px',
marginRight: -this.gutter/2 + 'px'
}
}
if (this.justify){
let key = ['start','end'].includes(this.justify)?`flex-${this.justify}`:this.justify
style = {
...style,
justifyContent:key
}
}
return style
}
}
}
</script>
<style lang="scss">
.ac-row{
display: flex;
flex-wrap: wrap;
overflow: hidden;
}
</style>
html
結構簡單,就是把傳入的呈現出來。props
方面也比較簡單,有一個 自定義校驗器。前面也說過了。解釋一下其餘的
-
mounted
。裏面 獲取全部子元素,吧gutter
賦給他們 -
...style
爲何要解構,防止裏面有樣式 -
這裏直接使用了 flex佈局。有精力得小夥伴能夠再補充一下浮動
col
<template>
<div class="ac-col" :class="colClass" :style="colStyle">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'ac-col',
data(){
return {
gutter:0
}
},
props:{
span:{
type:Number,
default:24
},
offset:{
type: Number,
default: 0
},
xs:[Number,Object],
sm:[Number,Object],
md:[Number,Object],
lg:[Number,Object],
xl:[Number,Object],
},
computed:{
colClass(){
let classes = []
classes.push(`ac-col-${this.span}`)
if (this.offset){
classes.push(`ac-col-offset-${this.offset}`)
}
['xs','sm','md','lg','xl'].forEach(type =>{
if (typeof this[type] === 'object'){
let {span,offset} = this[type]
span && classes.push(`ac-col-${type}-${span}`) // ac-col-xs-1
offset && classes.push(`ac-col-${type}-offset-${offset}`) // ac-col-xs-offset-1
}else {
//ac-col-xs-1
this[type] && classes.push(`ac-col-${type}-${this[type]}`)
}
})
return classes
},
colStyle(){
let style={}
if (this.gutter){
style = {
...style,
paddingLeft: this.gutter/2 + 'px',
paddingRight: this.gutter/2 + 'px'
}
}
return style
}
}
}
</script>
<style lang="scss">
/*經過循環24來創造寬度 sass語法*/
@import "./../../styles/_var";
/* 百分比佈局*/
@import "./../../styles/mixin";
@for $i from 1 through 24{
.ac-col-#{$i}{
width: $i/24*100%;
}
.ac-col-offset-#{$i}{
margin-left: $i/24*100%;
}
}
/*響應式佈局*/
@each $key in ('xs','sm','md','lg','xl'){
@for $i from 1 through 24{
@include res($key){
.ac-col-#{$key}-#{$i}{
width: $i/24*100%;
}
}
}
}
</style>
這段代碼的核心就是:經過計算屬性把不一樣的class
給加入到組件上
關於下面的 res
再上面通用代碼裏。就是一些sass
的應用
Container 容器組件
容器組件就相對來講簡單了。就是利用H5
新標籤。
裏面使用了flex
aside
<template>
<aside class="ac-aside" :style="`width:${width}`">
<slot></slot>
</aside>
</template>
<script>
export default {
name: 'ac-aside',
props: {
width: {
type: String,
default: '300px'
}
}
}
</script>
main
<template>
<main class="ac-main">
<slot></slot>
</main>
</template>
<script>
export default {
name: 'ac-main'
}
</script>
<style lang="scss">
.ac-main{
flex: 1;
padding: 20px;
}
</style>
header
<template>
<header class="ac-header" :style="height">
<slot></slot>
</header>
</template>
<script>
export default {
name: 'ac-header',
props: {
height: {
type: String,
default: '60px'
}
}
}
</script>
<style lang="scss">
.ac-header {
}
</style>
footer
<template>
<footer class="ac-footer" :style="height">
<slot></slot>
</footer>
</template>
<script>
export default {
name: 'ac-footer',
props: {
height: {
type: String,
default: '60px'
}
}
}
</script>
<style>
.ac-footer {
}
</style>
container
<template>
<section class="ac-container" :class="{isVertical}">
<slot></slot>
</section>
</template>
<script>
export default {
name: 'ac-container',
data() {
return {
isVertical: true
}
},
mounted() {
this.isVertical = this.$children.some(child=>
["ac-header", "ac-footer"].includes(child.$options.name)
)
}
}
</script>
<style lang="scss">
.ac-container {
display: flex;
flex-direction: row;
flex: 1;
}
.ac-container.isVertical {
flex-direction: column;
}
</style>
input 輸入框組件
參考element
,應該有如下功能
-
可狀況 -
密碼展現 -
帶圖標的輸入框 -
狀態禁用
<template>
<div class="ac-input" :class="elInputSuffix">
<ac-icon :icon="prefixIcon"
v-if="prefixIcon"
></ac-icon>
<input :type="ShowPassword?(password?'password':'text'):type" :name="name" :placeholder="placeholder"
:value="value"
@input="$emit('input',$event.target.value)"
:disabled="disabled" ref="input"
@change="$emit('change',$event)"
@blur="$emit('blur',$event)"
@focus="$emit('focus',$event)"
>
<!-- @mousedown.native.prevent 不會失去焦點-->
<ac-icon icon="qingkong"
v-if="clearable && value"
@click.native="$emit('input','')"
@mousedown.native.prevent
></ac-icon>
<!-- 先失去 再獲取焦點-->
<ac-icon icon="xianshimima"
v-if="ShowPassword && value"
@click.native="changeState"
></ac-icon>
<ac-icon :icon="suffixIcon"
v-if="suffixIcon"
></ac-icon>
</div>
</template>
<script>
export default {
name: 'ac-input',
data() {
return {
// 儘可能不要直接更改 父組件傳過來的值
password: true
}
},
props: {
type: {
type: String,
default: 'text'
},
name: {
type: String,
default: null
},
placeholder: {
type: String,
default: '請輸入內容'
},
value: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
},
clearable: {
type: Boolean,
default: false
},
ShowPassword: {
type: Boolean,
default: false
},
// 先後icon
prefixIcon: {
type: String
},
suffixIcon: {
type: String
}
},
computed: {
elInputSuffix() {
let classes = []
if (this.clearable || this.ShowPassword || this.suffixIcon) {
classes.push('ac-input-suffix-icon')
}
if (this.prefixIcon) {
classes.push('ac-input-prefix-icon')
}
return classes
}
},
methods: {
changeState() {
this.password = !this.password
this.$nextTick(()=>{
this.$refs.input.focus()
})
}
}
}
</script>
<style lang="scss">
.ac-input {
width: 180px;
display: inline-flex;
position: relative;
input {
border-radius: 4px;
border: 1px solid #dcdfe6;
color: #606266;
height: 40px;
line-height: 40px;
outline: none;
padding: 0 15px;
width: 100%;
&:focus {
outline: none;
border-color: #409eff;
}
&[disabled] {
cursor: not-allowed;
background-color: #f5f7fa;
}
}
}
.ac-input-suffix-icon {
.ac-icon {
position: absolute;
right: 6px;
top: 7px;
cursor: pointer;
}
}
.ac-input-prefix-icon {
input {
padding-left: 30px;
}
.ac-icon {
position: absolute;
left: 8px;
top: 12px;
cursor: pointer;
width: 16px;
height: 16px;
}
}
</style>
先看如下html
的代碼結構發現並不難,利用v-if
控制 ac-icon
的隱藏。利用props
傳入屬性來控制。計算屬性控制class
的添加
特別注意。記得在組件上寫@xxx="$emit('xxx',$event)"
。不然父類觸發不了事件
Upload 上傳組件
html結構
<template>
<div class="ac-upload">
<upLoadDrag v-if="drag" :accpet="accept" @file="uploadFiles">
</upLoadDrag>
<template v-else>
<div @click="handleClick" class="ac-upload-btn">
<slot></slot>
<!-- https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input/file 參考-->
<input class="input" type="file" :accept="accept" :multiple="multiple" :name=name
ref="input" @change="handleChange">
</div>
</template>
<!-- 提示文字-->
<div>
<slot name="tip"></slot>
</div>
<!-- 文件列表 -->
<ul>
<li v-for="(file,index) in files" :key="files.uid">
<div class="list-item">
<ac-icon icon="file"></ac-icon>
{{ file.name }}
<ac-progress v-if="file.status === 'uploading'" :percentage="file.percentage"></ac-progress>
<ac-icon icon="cuowu" @click.native="confirmDel(index)"></ac-icon>
</div>
</li>
</ul>
</div>
</template>
upLoadDrag
在後面拖拽上傳
type = file
時 參考
解釋一下 html
結構
-
根據傳入 drag
,判斷是否須要拖拽上傳 -
文件列表。根據不一樣的狀態來決定是否顯示 progress
js css 結構
css
就幾行。因此就直接寫在這裏面了
props
解釋
-
name
輸入框提交到後臺的名字 -
action
提交地址 -
:limit
限制提交個數 -
accept
類型 -
:on-exceed
超過提交個數 會執行次方法 -
:on-change
上傳文件發生狀態變化 會觸發 選擇文件 上傳成功等 -
:on-success
上傳成功時候觸發 -
:on-error
上傳失敗時候觸發 -
:on-progress
上傳過程當中時候觸發 -
:before-upload
上傳以前觸發的函數 -
:file-list
上傳文件列表 -
httpRequest
提供上傳方法,例如aixos
默認ajax
JS
可能這一長串代碼可能看的會頭疼。我先來串一下流程。
-
首先把 input
隱藏。點擊div
。觸發handleClick
方法,做用清空值,而且click
input
-
選擇文件後觸發 change
handleChange
事件。獲取文件列表, 開始準備上傳 -
uploadFiles
方法,獲取文件個數,經過handleFormat
格式化文件,而後經過upload
上傳 -
upload
判斷是否有beforeUpload
傳入,傳入執行,沒有就上傳 -
post
整合參數,開始上傳。
<script>
import upLoadDrag from './upLoad-drag'
import ajax from './ajax' // 本身寫的原生ajax
export default {
name: 'ac-upload',
props: {
name: {
type: String,
default: 'file'
},
action: {
type: String,
require: true
},
limit: Number,
fileList: {
type: Array,
default: ()=>[]
},
accept: String,
multiple: Boolean,
onExceed: Function,
onChange: Function,
onSuccess: Function,
onError: Function,
onProgress: Function,
beforeUpload: Function,
httpRequest: { // 提供上傳方法 默認ajax
type: Function,
default: ajax
},
drag: {
type: Boolean,
default: false
}
},
data() {
return {
tempIndex: 0,
files: [],
reqs: {}
}
},
components: {
upLoadDrag
},
watch: { // 監控 當傳入得時候 把用戶原來得文件也放到files裏面 而且格式化
fileList: {
immediate: true,
handler(fileList) {
this.files = fileList.map(item=>{
item.uid = Date.now() + this.tempIndex++
item.status = 'success'
return item
})
}
}
},
methods: {
handleClick() {
console.log(1)
// 點擊前先清空 防止屢次點擊
this.$refs.input.value = ''
this.$refs.input.click()
},
handleChange(e) {
// console.log(e) 從中 target 能夠找到
const files = e.target.files
console.log(files)
this.uploadFiles(files)
},
// 格式化
handleFormat(rawFile) {
rawFile.uid = Math.random() + this.tempIndex++
let file = { // 格式化信息
uid: rawFile.uid, //id
status: 'ready', // 狀態
name: rawFile.name, // 名字
raw: rawFile, // 文件
size: rawFile.size,
percentage: 0 //上傳進度
}
// 把當前用戶 上傳得文件放到列表中 一會要 展現出來
this.files.push(file)
// 接下來 通知文件變化
this.onChange && this.onChange(file)
},
upload(file) {
// 開始上傳
// 若是沒有限制 直接上傳 有限制得話 要進行判斷
if (!this.beforeUpload) {
console.log('上傳')
// 直接上傳
return this.post(file)
}
// 把文件傳給函數進行校驗 獲取結果
let result = this.beforeUpload(file)
console.log(result)
if (result) { // 返回true 纔有意義
// 直接上傳
return this.post(file)
}
},
uploadFiles(files) {
// 判斷上傳個數
if (this.limit && this.fileList.length + files.length > this.limit) {
return this.onExceed && this.onExceed(files, this.fileList)
}
[...files].forEach(file=>{
// 格式化文件 同一文件屢次上傳
this.handleFormat(file)
this.upload(file)
})
},
getFile(rawFile) {
return this.files.find(file=>file.uid === rawFile.uid)
},
handleProgress(ev, rawFile) {
let file = this.getFile(rawFile)
file.status = 'uploading'
file.percentage = ev.percent || 0
this.onProgress(ev, rawFile) // 觸發用戶定義
},
handleSuccess(res, rawFile) {
let file = this.getFile(rawFile)
file.status = 'success'
this.onSuccess(res, rawFile)
this.onChange(file)
},
handleError(err, rawFile) {
let file = this.getFile(rawFile)
file.status = 'fail'
this.onError(err, rawFile)
this.onChange(file)
// 移除文件
delete this.reqs[rawFile.uid]
},
post(file) {
// 上傳邏輯 調用上傳方法
// 整合一下參數 上傳須要傳遞參數
const uid = file.uid
// 配置項
const options = {
file: file,
fileName: this.name, // 傳入得名字
action: this.action,
onProgress: ev=>{
// 處理上傳中得過程
console.log('上傳中', ev)
this.handleProgress(ev, file)
},
onSuccess: res=>{
// 處理上傳成功後
console.log('上傳成功', res)
this.handleSuccess(res, file)
},
onError: err=>{
// 處理上傳失敗後
console.log('上傳失敗', err)
this.handleError(err, file)
}
}
console.log(options)
let req = this.httpRequest(options)
// 把每個ajax 存起來 能夠取消清求
this.reqs[uid] = req //
// 判斷結果 若是返回得是一個promise
if (req && req.then) {
req.then(options.onSuccess, options.onError)
}
},
confirmDel(index){
let res = confirm('確認刪除嗎')
console.log(this.files[index])
if (res){
this.files.pop(index)
}
}
}
}
</script>
<style lang="scss">
.ac-upload {
.ac-upload-btn {
display: inline-block;
}
.input {
display: none;
}
}
</style>
拖拽上傳
相比上面,這裏面就是改了一些 把click
改爲了drop
還有一些文件
<template>
<!-- @drop.prevent="onDrop" 鬆手以後 阻止默認行爲 防止打開文件
@dragover.prevent 劃過
@dragleave.prevent 離開
-->
<div class="ac-upload-drag"
@drop.prevent="onDrag"
@dragover.prevent
@dragleave.prevent
>
<ac-icon icon="shangchuan"></ac-icon>
<span>將文件拖拽到此區域</span>
</div>
</template>
<script>
export default {
name: 'upLoad-drag',
props:{
accept:{
type:String
}
},
methods:{
onDrag(e){
if (!this.accept){
this.$emit('file',e.dataTransfer.files)
}else {
// 本身過濾 過濾以後再次發送
this.$emit('file',e.dataTransfer.files)
}
}
}
}
</script>
<style lang="scss">
.ac-upload-drag{
background-color: #fff;
border: 1px dashed #d9d9d9;
border-radius: 6px;
width: 360px;
height: 180px;
cursor: pointer;
position: relative;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
.ac-icon{
width: 50px;
height: 70px;
}
}
</style>
原生ajax
export default function ajax(options) {
// 建立 對象
const xhr = new XMLHttpRequest()
const action = options.action
const fd = new FormData() // H5上傳文件API
fd.append(options.fileName,options.file)
// console.log(options.fileName,options.file)
// console.log('文件名'+options.fileName,options.file)
xhr.onerror = function (err){
options.onError(err) // 觸發錯誤回調
}
// 上傳完畢後走這個方法 H5 api
xhr.onload = function (){
let text = xhr.response || xhr.responseText
options.onSuccess(JSON.parse(text))
}
xhr.upload.onprogress = function(e){
if (e.total > 0){
e.percent = e.loaded/e.total * 100
}
options.onProgress(e)
}
// 開啓清求
xhr.open('post',action,true)
// 發送清求
xhr.send(fd)
return xhr
}
DatePick 日曆組件
日曆組件的 結構不是很難。可貴是 要去計算時間
思路解釋一下
-
input
聚焦後,執行handleFocus
函數,顯示下面得日曆框。點擊div
外面。執行handleBlur
。關閉日曆框
-
接下來是 content
的裏面的。顯示頭部,4個icon
外加時間顯示 -
接下來時日曆和時間
最主要可貴就時時間的顯示。得一步一步算。
每一個人的計算方式不同。這裏只給一個參照。
<template>
<div class="ac-date-pick" v-click-outside="handleBlur">
<ac-input suffix-icon="rili" @focus="handleFocus" :value="formatDate" placeholder="請選擇時間"
@change="handleChange"></ac-input>
<!-- content -->
<div class="ac-date-content" v-show="show">
<div class="ac-date-pick-content">
<!-- dates -->
<template v-if="mode === 'dates'">
<div class="ac-date-header">
<ac-icon icon="zuoyi" @click="changeYear(-1)"></ac-icon>
<ac-icon icon="zuo" @click="changeMonth(-1)"></ac-icon>
<span><b @click="mode='years'">{{ TemTime.year }}</b>年 <b @click="mode='months'">{{ TemTime.month+1 }}</b> 月</span>
<ac-icon icon="you" @click="changeMonth(1)"></ac-icon>
<ac-icon icon="youyi1" @click="changeYear(1)"></ac-icon>
</div>
<div>
<span v-for="week in weeks" :key="week" class="week">{{ week }}</span>
</div>
<div v-for="i in 6" :key="`row_${i}`">
<span v-for="j in 7" :key="`col_${j}`" class="week date-hover"
@click="selectDay(getCurrentMonth(i,j))"
:class="{
isNotCurrentMonth: !isCurrentMonth(getCurrentMonth(i,j)),
isToday:isToday(getCurrentMonth(i,j)),
isSelect:isSelect(getCurrentMonth(i,j))
}">
{{getCurrentMonth(i,j).getDate()}}
</span>
</div>
</template>
<!-- months -->
<template v-if="mode === 'months'">
<div class="ac-date-header">
<ac-icon icon="zuoyi" @click="changeYear(-1)"></ac-icon>
<span>
<b @click="mode='years'">{{ this.TemTime.year }}</b>年
</span>
<ac-icon icon="youyi1" @click="changeYear(1)"></ac-icon>
</div>
<div>
<div>
<span v-for="(i,index) in month" class="week date-hover year" @click="setMonth(index)">{{ i }}</span>
</div>
</div>
</template>
<!-- years -->
<template v-if="mode === 'years'">
<div class="ac-date-header">
<ac-icon icon="zuoyi" @click="changeYear(-10)"></ac-icon>
<span>
<b @click="mode='years'">{{ startYear() }}</b>年-
<b @click="mode='years'">{{ startYear()+10 }}</b>年
</span>
<ac-icon icon="youyi1" @click="changeYear(10)"></ac-icon>
</div>
<div>
<div>
<span v-for="i in showYears" class="week date-hover year"
@click="setYear(i)"
>{{ i.getFullYear() }}</span>
</div>
</div>
</template>
</div>
</div>
</div>
</template>
<script>
function getTime(date) {
let year = date.getFullYear()
let month = date.getMonth()
let day = date.getDate()
return [year, month, day]
}
import clickOutside from 'v-click-outside'
export default {
name: 'ac-date-pick',
data() {
let [year, month, day] = getTime(this.value || new Date())
return {
show: false,
mode: 'dates',
weeks: ['日', '一', '二', '三', '四', '五', '六'],
month: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
time: { // 負責展現
year, month, day
},
TemTime: { // 臨時時間 修改這個 由於time 是經過父級傳入的值計算出來的 負責修改
year, month, day
}
}
},
watch: {
value(newValue) {
console.log(newValue)
let [year, month, day] = getTime(newValue)
console.log(year, month, day)
this.time = {
year, month, day
}
this.TemTime = { ...this.time }
}
},
computed: {
showDate() {
let firstDay = new Date(this.TemTime.year, this.TemTime.month, this.TemTime.day)
// console.log(firstDay)
let weekDay = firstDay.getDay() // 獲取周幾 0 - 6
// console.log(weekDay)
let day = firstDay.getDate()
// console.log(parseInt((day - weekDay) / 7) + 1)
weekDay = weekDay === 0 ? 7 : weekDay
let start = firstDay - weekDay * 1000 * 60 * 60 * 24 - 7 * (parseInt((day - weekDay) / 7) + 1) * 1000 * 60 * 60 * 24
let arr = []
for (let i = 0; i < 42; i++) {
arr.push(new Date(start + i * 1000 * 60 * 60 * 24))
}
return arr
},
showYears(){
let arr = []
for (let i = 0; i < 10; i++) {
let startYear = new Date(this.startYear(),1)
arr.push(new Date(startYear.setFullYear(startYear.getFullYear() + i)))
}
return arr
},
formatDate() {
if (this.value) {
console.log('這個是爲了確認父級是否傳值。不傳就不渲染input裏面的值')
// padStart padEnd 補全長度的功能。若是某個字符串不夠指定長度,會在頭部或尾部補全
return `${ this.time.year }-${ (this.time.month + 1 + '').padStart(2, 0) }-${ (this.time.day + '').padStart(2, 0) }`
}
}
},
directives: {
clickOutside: clickOutside.directive
},
props: {
value: [String, Date],
default: ()=>new Date()
},
methods: {
handleFocus() { // 控制點擊輸入框彈出浮層
this.show = true
console.log('focus')
},
handleBlur() { // 當點擊 div外側的時候 隱藏浮層
this.show = false
this.mode = 'dates'
console.log('Blur')
},
getCurrentMonth(i, j) {
return this.showDate[(i - 1) * 7 + (j - 1)]
},
getTenYears(i,j){
if (((i - 1) * 4 + (j - 1)) < 10){
return this.showYears[(i - 1) * 4 + (j - 1)]
}
},
isCurrentMonth(date) {
let { year, month } = this.TemTime
let [y, m] = getTime(date)
// console.log(year,month)
// console.log(y,m)
return year === y && month === m
},
isToday(date) {
let [year, month, day] = getTime(date)
let [y, m, d] = getTime(new Date)
return year === y && month === m && day === d
},
selectDay(date) {
this.$emit('input', date)
this.handleBlur()
},
isSelect(date) {
let { year, month, day } = this.time
let [y, m, d] = getTime(date)
return year === y && month === m && day === d
},
changeYear(count) {
let oldDate = new Date(this.TemTime.year, this.TemTime.month)
let newDate = oldDate.setFullYear(oldDate.getFullYear() + count)
let [year] = getTime(new Date(newDate))
this.TemTime.year = year
// this.TemTime.year += mount //這樣改容易有bug
},
changeMonth(count) {
let oldDate = new Date(this.TemTime.year, this.TemTime.month)
let newDate = oldDate.setMonth(oldDate.getMonth() + count)
let [year, month] = getTime(new Date(newDate))
this.TemTime.year = year
this.TemTime.month = month
},
handleChange(e) {
console.log(e.target.value)
let newValue = e.target.value
let regExp = /^(\d{4})-(\d{1,2})-(\d{1,2})$/
if (newValue.match(regExp)) {
// console.log(RegExp.$1,RegExp.$2,RegExp.$3)
this.$emit('input', new Date(RegExp.$1, RegExp.$2 - 1, RegExp.$3))
} else {
e.target.value = this.formatDate
}
},
startYear() {
return this.TemTime.year - this.TemTime.year % 10
},
setYear(date){
this.TemTime.year = date.getFullYear()
this.mode = 'months'
},
setMonth(index){
this.TemTime.month = index
this.mode = 'dates'
}
}
}
</script>
<style lang="scss">
@import "../../styles/var";
@import "../../styles/mixin";
.ac-date-pick {
border: 1px solid red;
display: inline-block;
.ac-date-content {
position: absolute;
z-index: 10;
user-select: none;
width: 280px;
background: #fff;
box-shadow: 1px 1px 2px $primary, -1px -1px 2px $primary;
.ac-date-header {
height: 40px;
@include flexSet()
}
.ac-date-pick-content {
.week {
width: 40px;
height: 40px;
display: inline-block;
text-align: center;
line-height: 40px;
border-radius: 50%;
}
.year{
width: 70px;
height: 70px;
line-height: 70px;
}
.date-hover:hover:not(.isNotCurrentMonth):not(.isSelect) {
color: $primary;
}
.isNotCurrentMonth {
color: #ccc;
}
.isSelect {
background-color: $primary;
color: #fff;
}
.isToday {
background-color: #fff;
color: $primary
}
}
}
}
</style>
Switch 開關組件
switch
就相對簡單一點。純樣式控制。input
寫到 label
內,不須要寫for
了。經過僞類
控制。
經過computed
來控制class
樣式添加
<template>
<div class="ac-switch">
<span v-if="activeText" :class="{checkedText:!checked}">{{ activeText }}</span>
<label class="ac-label" :style="labelStyle">
<input type="checkbox" :checked="checked" @click="changCheck" :disabled="disabled">
<span></span>
</label>
<span v-if="inactiveText" :class="{checkedText:checked}">{{ inactiveText }}</span>
</div>
</template>
<script>
export default {
name: 'ac-switch',
props: {
value: {
type: Boolean,
default: false
},
activeText: String,
inactiveText: String,
activeColor:{
type: String,
default:'rgb(19, 206, 102)'
},
inactiveColor: String,
disabled:{
type: Boolean,
default:false
}
},
data() {
return {
checked: this.value
}
},
methods: {
changCheck() {
this.checked = !this.checked
this.$emit('input', this.checked)
}
},
computed:{
labelStyle(){
let style = {}
if (this.checked){
style.backgroundColor = this.activeColor
}else {
style.backgroundColor = this.inactiveColor
}
if (this.disabled){
style.cursor = 'not-allowed'
style.opacity = 0.6
}
return style
}
}
}
</script>
<style lang="scss">
.ac-label {
width: 40px;
height: 20px;
border-radius: 30px;
overflow: hidden;
vertical-align: middle;
position: relative;
display: inline-block;
background: #ccc;
box-shadow: 0 0 1px #36a6d4;
input {
visibility: hidden;
}
span {
position: absolute;
top: 0;
left: 0;
border-radius: 50%;
background: #fff;
width: 50%;
height: 100%;
transition: all linear 0.2s;
}
input:checked + span {
transform: translateX(100%);
}
}
.checkedText {
color: #3a8ee6;
}
</style>
infinteScroll 無限滾動指令
無限滾動不能做爲一個組件。因此放成一個指令。參考地址
-
attributes
自定義的默認屬性 -
getScrollContainer
獲取Scroll的容器元素 -
getScrollOptions
屬性合併 -
handleScroll
控制是否Scroll
思路。插入的時候 獲取fn
和vnode
.再獲取容器。獲取參數。綁定事件。最後解除綁定
重點說一下 MutationObserver MDN
import throttle from 'lodash.throttle'
// 自定義屬性
const attributes = {
delay: {
default: 200
},
immediate: {
default: true
},
disabled: {
default: false
},
distance: {
default: 10
},
}
/**
* 獲取Scroll的容器元素
* @param el 元素節點
* @returns {(() => (Node | null))|ActiveX.IXMLDOMNode|(Node & ParentNode)|Window}
*/
const getScrollContainer = (el)=>{
let parent = el
while (parent) {
if (document.documentElement === parent) {
return window
}
// 獲取元素是否有 overflow屬性
const overflow = getComputedStyle(parent)['overflow-y']
if (overflow.match(/scroll|auto/)) {
return parent
}
parent = parent.parentNode
}
}
/**
* 拿到傳入的屬性和默認屬性進行比對 合併
* @param el 節點
* @param vm Vue實例
* @returns {{}} 合併後的屬性
*/
const getScrollOptions = (el, vm)=>{
// entries參考網址 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
return Object.entries(attributes).reduce((map, [key, option])=>{
let defaultValue = option.default
let userValue = el.getAttribute(`infinite-scroll-${ key }`)
map[key] = vm[userValue] ? vm[userValue] : defaultValue
return map
}, {})
}
const handleScroll = function(cb) {
let { container, el, vm,observer } = this['infinite-scroll'] // 綁定了this
let { disabled,distance } = getScrollOptions(el, vm)
if (disabled) return
let scrollBottom = container.scrollTop + container.clientHeight
if (container.scrollHeight - scrollBottom <= distance){
cb()
}else {
if (observer){ // 接觸監控
observer.disconnect()
this['infinite-scroll'].observer = null
}
}
}
export default {
name: 'infinite-scroll',
inserted(el, bindings, vNode) { // vNode裏面有context能夠訪問上下文
// 插入 指令生效
console.log('指令生效')
console.log(bindings.value) // 獲取到fn
console.log(vNode.context) // 獲取虛擬實例 裏面有屬性
let cb = bindings.value
let vm = vNode.context
// 1. 開始尋找循環的容器
let container = getScrollContainer(el)
console.log(container)
if (container !== window) {
console.log('綁定事件')
// 2. 獲取Options
let { delay, immediate } = getScrollOptions(el, vm)
// 3. 執行函數 節流 增長滾動事件
let onScroll = throttle(handleScroll.bind(el, cb), delay)
el['infinite-scroll'] = {
container,
onScroll, el, vm
}
if (immediate) {
const observe =el['infinite-scroll'].observer= new MutationObserver(onScroll) // 觀察頁面是否繼續加載
observe.observe(container, {
childList: true, // 監控孩子列表發生變化
subtree: true // 當子dom元素 發生變化也觸發
})
onScroll() // 默認先加載
}
container.addEventListener('scroll', onScroll)
}
},
unbind(el) {
// 解除
const { container, onScroll } = el
if (container) {
container.removeEventListener('scroll', onScroll)
el['infinite-scroll'] = {}
}
}
}
Message 通知組件
這裏面有兩個。爲何又兩個,由於message
是經過appendChild
添加到Dom
裏面的
思路
-
經過 extend
方法生成一個vue
子類。而後經過$mount
生成dom對象
再添加到document
-
options.close
在element
方法裏不是這樣寫的還有一部分判斷等。這裏接直接偷懶了,能正常使用
index
-
由於可能要有多個 message
。須要計算高度。因此使用了 數組存放。根據個數循環高度
import Vue from 'vue'
import MessageCom from './Message.vue';
let instances = []
// 生成一個vue 的 子類
let MessageConstructor = Vue.extend(MessageCom)
// 參考element 的寫法 作了必定的修改和簡化
const Message = (options)=>{
options.close = function() {
let length = instances.length
instances.splice(0, 1);
for (let i = 0; i < length - 1; i++) {
let removedHeight = instances[i].$el.offsetHeight;
let dom = instances[i].$el;
dom.style['top'] =
parseInt(dom.style['top'], 10) - removedHeight - 16 + 'px';
}
}
let instance = new MessageConstructor({
data: options,
})
instance.$mount()
document.body.appendChild(instance.$el)
let verticalOffset = 20;
instances.forEach(item=>{
verticalOffset += item.$el.offsetHeight + 16; // 53 +16
});
instance.verticalOffset = verticalOffset;
instance.visible = true
instances.push(instance)
return instance
}
// 加載 'warning', 'error', 'success', 'info' 等
['warning', 'error', 'success', 'info'].forEach(type=>{
Message[type] = function(options) {
options.type = type
return Message(options)
}
})
export default Message
message
這個裏面沒有什麼較難的內容。基本就是樣式的控制
<template>
<transition name="ac-message-fade">
<div v-show="visible"
class="ac-message"
:style="messageStyle"
:class="MesClass"
>
{{ message }}
</div>
</transition>
</template>
<script>
export default {
name: 'Message',
data() {
return {
message: '',
type: '',
visible: false,
duration: 3000,
verticalOffset: 0
}
},
mounted() {
if (this.duration > 0)
setTimeout(()=>{
this.$destroy() // 銷燬當前實例
// 銷燬dom 元素
this.$el.parentNode.removeChild(this.$el)
this.close()
}, this.duration)
},
computed: {
messageStyle() {
let style = {}
style.top = this.verticalOffset + 'px'
style.zIndex = 2000 + this.verticalOffset
return style
},
MesClass() {
const classes = []
if (this.type) {
classes.push(`ac-message-${ this.type }`)
}
return classes
}
}
}
</script>
<style lang="scss">
@import "../../styles/var";
.ac-message {
min-width: 380px;
border-radius: 4px;
border: 1px solid #ebeef5;
position: fixed;
left: 50%;
background-color: #edf2fc;
transform: translateX(-50%);
transition: opacity .3s, transform .4s, top .4s;
overflow: hidden;
padding: 15px 15px 15px 20px;
display: flex;
align-items: center;
@each $type, $color in (success:$success, error:$danger, warning:$warning, info:$info) {
&-#{$type} {
color: $color;
}
}
&-success {
background-color: #f0f9eb;
border-color: #e1f3d8
}
&-warning {
background-color: #fdf6ec;
border-color: #faecd8
}
&-error {
background-color: #fef0f0;
border-color: #fde2e2
}
}
.ac-message-fade-enter, .ac-message-fade-leave-active {
opacity: 0;
transform: translate(-50%, -100%)
}
</style>
Popover 彈出框組件
這個組件跟Message
差很少。並不難。主要對JS
三你們族的的引用。得到元素位置。根據元素位置來肯定popover
的位置
@click.stop
阻止事件冒泡
我的以爲這一部分寫的有一點冗餘。感受能夠用offset
搞定所有的。可是沒有使用。就先這樣吧
<template>
<div class="ac-popover" ref="parent">
<!-- 阻止事件冒泡-->
<div class="ac-popover-content"
v-show="show"
:class="`popover-${this.placement}`"
:style="position"
ref="content"
@click.stop>
<h3 v-if="title">{{ title }}</h3>
<slot>{{ content }}</slot>
<div class="popover"></div>
</div>
<div ref="reference">
<slot name="reference"></slot>
</div>
</div>
</template>
<script>
const on = (element, event, handler)=>{
element.addEventListener(event, handler, false)
}
const off = (element, event, handler)=>{
element.removeEventListener(event, handler, false)
}
export default {
name: 'ac-popover',
data() {
return {
show: this.value,
clientWidth: 0,
offsetTop: 0,
offsetLeft: 0
}
},
props: {
value: {
type: Boolean,
default: false
},
placement: {
validator(type) {
if (!['top', 'bottom', 'left', 'right'].includes(type)) {
throw new Error('屬性必須是' + ['top', 'bottom', 'left', 'right'].join(','))
}
return true
}
},
width: {
type: [String, Number],
default: '200px'
},
content: {
type: String,
default: ''
},
title: {
type: String,
default: ''
},
trigger: {
type: String,
default: ''
},
},
methods: {
handleShow() {
this.show = !this.show
},
handleDom(e) {
if (this.$el.contains(e.target)) {
return false
}
this.show = false
},
handleMouseEnter() {
clearTimeout(this.time)
this.show = true
},
handleMouseLeave() {
this.time = setTimeout(()=>{
this.show = false
}, 200)
}
},
watch: {
show(value) {
if (value && this.trigger === 'hover') {
this.$nextTick(()=>{
let content = this.$refs.content
document.body.appendChild(content)
on(content, 'mouseenter', this.handleMouseEnter)
on(content, 'mouseleave', this.handleMouseLeave)
})
}
}
},
computed: {
position() {
let style = {}
let width
if (typeof this.width === 'string') {
width = this.width.split('px')[0]
} else {
width = this.width
}
if (this.trigger === 'click') {
if (this.placement === 'bottom' || this.placement === 'top') {
style.transform = `translate(-${ this.clientWidth / 2 }px,-50%)`
style.right = `-${ width / 2 }px`
// console.log(style.right)
} else {
style.top = '-21px'
}
if (this.placement === 'bottom') {
style.top = '-100%'
} else if (this.placement === 'top') {
style.top = '200%'
} else if (this.placement === 'left') {
style.left = '104%'
} else if (this.placement === 'right') {
console.log('click'+this.offsetLeft)
style.left = '-190%'
}
} else if (this.trigger === 'hover') {
if (this.placement === 'bottom' || this.placement === 'top') {
style.left = `${ this.offsetLeft - width / 2 }px`
style.transform = `translateX(${ this.clientWidth / 2 }px)`
} else {
style.top = `${ this.offsetTop - 21 }px`
}
if (this.placement === 'bottom') {
style.top = `${ this.offsetTop - 73 }px`
} else if (this.placement === 'top') {
style.top = `${ this.offsetTop + 49 }px`
} else if (this.placement === 'left') {
console.log(width)
style.left = `${ this.offsetLeft + this.clientWidth + 7 }px`
} else if (this.placement === 'right') {
style.left = `${ this.offsetLeft - width - 6 }px`
}
}
return style
}
},
mounted() {
let reference = this.$slots.reference
console.log(this.$refs.parent.offsetLeft)
this.offsetTop = this.$refs.parent.offsetTop
this.offsetLeft = this.$refs.parent.offsetLeft
this.clientWidth = this.$refs.reference.clientWidth
if (reference) {
// console.log(reference) // 獲取dom節點
this.reference = reference[0].elm
}
if (this.trigger === 'hover') {
on(this.$el, 'mouseenter', this.handleMouseEnter)
on(this.$el, 'mouseleave', this.handleMouseLeave)
} else if (this.trigger === 'click') {
on(this.reference, 'click', this.handleShow)
on(document, 'click', this.handleDom)
}
},
beforeDestroy() {
off(this.$el, 'mouseenter', this.handleMouseEnter)
off(this.$el, 'mouseleave', this.handleMouseLeave)
off(this.reference, 'click', this.handleShow)
off(document, 'click', this.handleDom)
}
}
</script>
<style lang="scss">
.ac-popover {
position: relative;
display: inline-block;
}
.ac-popover-content {
width: 200px;
position: absolute;
padding: 10px;
top: 0;
background-color: #fff;
border-radius: 5px;
box-shadow: -1px -1px 3px #ccc, 1px 1px 3px #ccc;
z-index: 2003;
}
.popover {
position: absolute;
&::after, &::before {
content: '';
display: block;
width: 0;
height: 0;
border: 6px solid #ccc;
position: absolute;
border-left-color: transparent;
border-top-color: transparent;
border-right-color: transparent;
}
&::after {
border-bottom-color: #fff;
/*https://www.runoob.com/cssref/css3-pr-filter.html*/
filter: drop-shadow(0 -2px 1px #ccc);
}
}
.popover-bottom {
.popover {
left: 50%;
margin-left: -6px;
bottom: 0;
&::after, &::before {
transform: rotate(180deg);
}
}
}
.popover-top {
.popover {
left: 50%;
margin-left: -6px;
top: -12px;
}
}
.popover-left {
.popover {
top: 50%;
margin-left: -6px;
left: -6px;
&::after, &::before {
transform: rotate(-90deg);
}
}
}
.popover-right {
.popover {
top: 50%;
margin-left: -6px;
right: 0;
&::after, &::before {
transform: rotate(90deg);
}
}
}
</style>
分頁組件
分頁組件。相比較可貴地方就在。須要計算何時顯示。何時不應顯示。即pagers
計算屬性。吧這一部分理解了,基本也就沒什麼了/。主要是一個計算問題
<template>
<ul class="ac-pagination">
<li>
<ac-icon icon="zuo" @click="select(currentPage - 1)" :class="{noAllow: currentPage === 1 }"></ac-icon>
</li>
<li><span :class="{active:currentPage === 1}" @click="select(1)">1</span></li>
<li v-if="showPrev"><span>...</span></li>
<li v-for="p in pagers" :key="p">
<span :class="{active:currentPage === p}" @click="select(p)">
{{p}}
</span>
</li>
<li v-if="showNext"><span>...</span></li>
<li><span :class="{active:currentPage === total}" @click="select(total)">{{ total }}</span></li>
<li>
<ac-icon icon="you" @click="select(currentPage + 1)" :class="{noAllow:currentPage===total}"></ac-icon>
</li>
</ul>
</template>
<script>
export default {
name: 'ac-pagination',
data() {
return {
showPrev: false,
showNext: false
}
},
methods:{
select(current){
if (current <1){
current = 1
}else if (current > this.total){
current = this.total
}else if (current !== this.currentPage){
this.$emit('update:current-page',current)
}
}
},
props: {
total: {
type: Number,
default: 1
},
pageCount: {
type: Number,
default: 7
},
currentPage: {
type: Number,
default: 1
}
},
computed: {
// 最多顯示 7個
// 1 2 3 4 5 6 ...10
// 1 .。3 4 5 6 7 .。。10
pagers() {
// floor向下取整 ceil 向上取整
let middlePage = Math.ceil(this.pageCount / 2)
let showPrev = false
let showNext = false
if (this.total > this.pageCount) {
if (this.currentPage > middlePage) {
showPrev = true
}
if (this.currentPage < this.total - middlePage + 1) {
showNext = true
}
}
let arr = []
if (showPrev && !showNext) {
// 前面存在。。。
let start = this.total - (this.pageCount - 2)
for (let i = start; i < this.total; i++) {
arr.push(i)
}
} else if (showNext && showPrev) {
let count = Math.floor((this.pageCount - 2) / 2)
for (let i = this.currentPage - count; i <= this.currentPage + count; i++) {
arr.push(i)
}
} else if (!showPrev && showNext) {
// 後面存在...
for (let i = 2; i < this.pageCount; i++) {
arr.push(i)
}
} else {
for (let i = 2; i < this.total; i++) {
arr.push(i)
}
}
this.showPrev = showPrev
this.showNext = showNext
return arr
}
}
}
</script>
<style lang="scss">
.ac-pagination {
li {
user-select: none;
list-style: none;
display: inline-flex;
vertical-align: middle;
min-width: 35.5px;
padding: 0 4px;
background: #fff;
.active {
color: #3a8ee6;
}
}
.noAllow{
cursor: not-allowed;
}
}
</style>
table 表格組件
表格做爲一個最經常使用的組件。
着重說一下 固定表頭的作法
-
先獲取到表頭的 Dom
-
空出一部份距離。再把 thead
插入進包裹的地方 便可完成
<template>
<div class="ac-table" ref="wrapper">
<div class="table-wrapper" ref="tableWrapper" :style="{height}">
<table ref="table">
<thead>
<tr>
<th v-for="item in CloneColumn" :key="item.key">
<div v-if="item.type && item.type === 'select'">
<input type="checkbox" :style="{width: item.width + 'px'}" :checked="checkAll" ref="checkAll"
@click="checkAllStatus">
</div>
<span v-else>
{{ item.title }}
<span v-if="item.sortable" @click="sort(item,item.sortType)">
<ac-icon icon="sort"></ac-icon>
</span>
</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row,index) in CloneData" :key="index">
<td v-for="(col,index) in CloneColumn" :key="index">
<div v-if="col.type && col.type === 'select'">
<input type="checkbox" :style="{width: col.width+'px'}" @click="selectOne($event,row)"
:checked="checked(row)">
</div>
<div v-else>
<div v-if="col.slot">
<slot :name="col.slot" :row="row" :col="col"></slot>
</div>
<div v-else>
{{ row[col.key] }}
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
export default {
name: 'ac-table',
data() {
return {
CloneColumn: [],
CloneData: [],
checkedList: []
}
},
created() {
this.CloneColumn = [...this.columns]
this.CloneData = [...this.data]
this.CloneData = this.CloneData.map(item=>{
item._id = Math.random()
return item
})
this.CloneColumn = this.CloneColumn.map(item=>{
item.sortType = item.sortType ? item.sortType : 0
this.sort(item, item.sortType)
return item
})
},
props: {
columns: {
type: Array,
default: ()=>[]
},
data: {
type: Array,
default: ()=>[]
},
height: {
type: String
}
},
methods: {
checked(row) {
return this.checkedList.some(item=>item._id === row._id)
},
selectOne(e, selectItem) {
if (e.target.checked) {
this.checkedList.push(selectItem)
} else {
// 沒有標識 須要去除 添加標識
this.checkedList = this.checkedList.filter(item=>item._id !== selectItem._id
)
}
this.$emit('on-select', this.checkedList, selectItem)
},
checkAllStatus(e) {
this.checkedList = e.target.checked ? this.CloneData : []
this.$emit('on-select-all', this.checkedList)
},
sort(col, type) {
let data = [...this.CloneData]
if (type !== 0) {
let key = col.key
if (type === 1) {
data.sort((a, b)=>{
return a[key] - b[key]
})
} else if (type === 2) {
data.sort((a, b)=>{
return b[key] - a[key]
})
}
this.CloneData = data
}
this.$emit('on-list-change', data, col.sortType)
col.sortType = col.sortType === 1 ? 2 : 1
}
},
computed: {
checkAll() {
return this.CloneData.length === this.checkedList.length
}
},
watch: {
checkedList() {
if (this.CloneData.length !== this.checkedList.length) {
if (this.checkedList.length > 0)
return this.$refs.checkAll[0].indeterminate = true
}
this.$refs.checkAll[0].indeterminate = false
}
},
mounted() {
if (this.height) {
let wrapper = this.$refs.wrapper
let tableWrapper = this.$refs.tableWrapper
let table = this.$refs.table
let cloneTable = table.cloneNode()
console.log(cloneTable)
let thead = table.children[0]
console.log(thead.getBoundingClientRect())
tableWrapper.style.paddingTop = thead.getBoundingClientRect().height + 'px'
cloneTable.style.width = table.offsetWidth + 'px'
cloneTable.appendChild(thead)
cloneTable.classList.add('fix-header')
// 設置對其 querySelector獲取文檔種DOM元素
let tds = table.querySelector('tbody tr').children
console.log(tds)
let ths = cloneTable.querySelector('thead tr').children
tds.forEach((item, index)=>{
ths[index].style.width = item.getBoundingClientRect().width + 'px'
})
wrapper.appendChild(cloneTable)
}
}
}
</script>
<style lang="scss">
.ac-table {
position: relative;
overflow: hidden;
.fix-header {
position: absolute;
top: 0;
}
.table-wrapper {
overflow-y: scroll;
}
table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
thead {
th {
background-color: #f8f8f9;
white-space: nowrap;
}
}
tbody {
tr:hover {
background-color: #7dbcfc;
}
}
th, td {
border-bottom: 1px solid #ddd;
padding: 10px;
text-align: left;
}
}
}
</style>
Vuepress 配置
有關Vuepress
不作過多的解釋了。官網 直接進入正題
貼一下本身的
導航欄配置
官方文檔
總結
本篇文章介紹了部分組件的我的開發過程。學習到的
-
關於 sass
語法的使用。 -
還有就是組件設計時考慮的全面與否 -
一些組件的設計遇到的麻煩。再閱讀源碼以後解決。必定的獨立思考和解決能力 -
不一樣組件的寫法。 -
Vuepress
的配置
抒發迷茫
-
常常被打擊。不知道將來到底須要做什麼。 -
身爲一個前端工程師。沒有什麼拿得出手的做品。 -
東西突飛猛進。有好多東西本身還有去學。小程序, flutter
等。感受到有點累 -
優化策略 沒接觸或者實際操做過。 -
想去接觸一下實際的工做。不想再去模仿。 -
繼續加油吧
本文分享自微信公衆號 - 阿琛前端成長之路(lwkWyc)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。