最简单的办法,就是通过css
隐藏,比如给该联动选择器,也就是poperclass
加一个特别的类名show-last-cascader
:
.show-last-cascader {
.el-cascader-menu{
.el-checkbox {
display:none;
}
}
}
然后你需要做一次判断expand-change
这个事件,去做判断,具体参考这个demo
问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性,临之以利而观其廉,期之以事而观其信。
夕水 回答了问题 · 1月14日
最简单的办法,就是通过css
隐藏,比如给该联动选择器,也就是poperclass
加一个特别的类名show-last-cascader
:
.show-last-cascader {
.el-cascader-menu{
.el-checkbox {
display:none;
}
}
}
然后你需要做一次判断expand-change
这个事件,去做判断,具体参考这个demo
关注 5 回答 3
夕水 回答了问题 · 2020-09-15
你应该就是想做类似TAB选项卡
的功能吧,按你的需求来看,只需悬浮就可以添加选中类名。
var lis = document.querySelectorAll(".sci_ban_tit");
lis.forEach(item => {
item.onmouseover = () => {
//先全部清空选中,再给当前添加选中
lis.forEach(item => item.classList.remove('active'));
item.classList.add('active');
}
})
我想你需要的是这样的代码,不过用css
也可以实现啊?
关注 6 回答 6
夕水 回答了问题 · 2020-09-07
this.$refs.seamlessScroll
指向的是vue-seamless-scroll
这个组件实例,如果添加到DOM
元素上,则指的是该DOM
元素,与this
是不一样的,详见vm.$refs用法。
关注 4 回答 3
夕水 发布了文章 · 2020-09-06
PS:npm速度慢可使用cnpm
第一步,让我们先把项目环境搭建好,首先打开命令窗口,执行如下命令:
npm init
搭建好了package.json
文件之后,接下来开始装依赖包,我们需要用到webpack webpack-cli
来打包项目,执行如下命令:
npm install webpack webpack-cli --save-dev
在编写代码时,我们需要用到es6
的语法,因此我们还需要安装@babel/core @babel/cli @babel/preset-env babel-loader
依赖来处理es6
兼容语法。继续执行如下命令:
npm install --save-dev @babel/core @babel/cli @babel/preset-env babel-loader
接下来,创建一个babel.config.json
文件,然后写入如下代码:
{
"presets": [
[
"@babel/env",
{
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1",
},
"useBuiltIns": "usage",
}
]
]
}
这只是一个默认的配置,也可以自行根据需求来进行配置,更多信息详见babel文档。
这还没有结束,我们还需要搭建vue
的开发环境,我们需要编译.vue
,所以我们需要安装vue-loader vue-template-compiler vue
等依赖包,继续执行如下命令:
npm install vue vue-loader vue-template-compiler --save-dev
我们目前所需要的依赖就暂时搭建完成,接下来在页面根目录创建一个index.html
文件,写入如下代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue-cli</title>
</head>
<body>
<div id="app"></div>
</body>
<script data-original="/build.js"></script>
</html>
在这里,我们注意到了我们打包最后引入的文件为build.js
文件,接下来我们开始编写webpack
的配置,在根目录下继续创建一个webpack.config.js
文件,代码如下:
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
mode:"development",
entry:'./main.js',
output:{
path:__dirname,
filename:'build.js'
},
module:{
rules:[
{
test: /\.vue$/,
loader: "vue-loader"
},
{
test: /\.js$/,
loader: "babel-loader",
exclude: /node_modules/
}
]
},
plugins: [
new VueLoaderPlugin()
]
}
在导出后面还需要加上这一段js
代码,如下:
resolve: {
alias: {
'vue': 'vue/dist/vue.js'
}
}
为什么要加上这一个配置,这个后面会说明原因,这里暂时先放置,继续在根目录下分别创建一个App.vue
与main.js
文件。代码分别如下:
import Vue from 'vue';
import App from './App.vue'
Vue.config.productionTip = false;
var vm = new Vue({
el: "#app",
// render:(h) => { return h(App)},
components: {
App
},
template: "<App />"
})
<template>
<div id="app">
<p>{{ msg }}</p>
</div>
</template>
<script>
export default {
data() {
return {
msg: "hello,vue.js!"
};
},
mounted() {
},
methods: {
}
};
</script>
接下来,执行命令webpack
,然后我们就可以看到页面中会生成一个build.js
文件,然后运行index.html
文件,我们就可以在浏览器页面上看到hello,vue.js!
的字符串,稍等,我们似乎忘记了什么,一般在开发中,谁会给你运行webpack
命令来打包,不都是执行npm run build
嘛,让我们在package.json
中加上这一行代码
{
"name": "timeline-project",
"version": "1.0.0",
"description": "a component with vue.js",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack" //这里是添加的代码
},
"keywords": [
"timeline"
],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/cli": "^7.11.5",
"@babel/core": "^7.11.5",
"@babel/preset-env": "^7.11.5",
"babel-loader": "^8.1.0",
"vue": "^2.6.12",
"vue-loader": "^15.9.3",
"vue-template-compiler": "^2.6.12",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12"
}
}
等等,我们还忘了一件事,别人都可以使用npm run dev
命令来在本地运行项目,我们为什么不可以呢?我们需要安装webpack-dev-server
依赖,执行如下命令安装:
npm install webpack-dev-server --save-dev
安装完成,让我们继续在package.json
中添加这样一行代码,如下所示:
{
"name": "timeline-project",
"version": "1.0.0",
"description": "a component with vue.js",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"dev": "webpack-dev-server --open --hot --port 8081" //这里是添加的代码
},
"keywords": [
"timeline"
],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/cli": "^7.11.5",
"@babel/core": "^7.11.5",
"@babel/preset-env": "^7.11.5",
"babel-loader": "^8.1.0",
"vue": "^2.6.12",
"vue-loader": "^15.9.3",
"vue-template-compiler": "^2.6.12",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12"
}
}
添加的代码很好理解,就是启动服务热更新,并且端口是8081
。接下来让我们尝试运行npm run dev
,看看发生了什么!
真的很棒,我们已经成功运行了vue-cli
项目,搭建环境这一步到目前为止也算是成功了。
前面还提到一个问题,那就是在webpack.config.js
中为什么要加上resolve
配置,这是因为,如果我们需要在.vue
文件中使用components
选项来注册一个组件的话,就必须要引入完整的vue.js
,也就是编译模板代码,如果我们只用render
来创建一个组件,那么就不需要添加这个配置,这就是官网所说的运行时 + 编译器 vs. 只包含运行时。
在这里我们还要注意一个问题,那就是我们需要处理单文件组件中的css
样式,所以我们需要安装css-loader与style-loader
依赖。执行如下命令:
npm install style-loader css-loader --save-dev
在webpack.config.js
中添加如下代码:
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
mode:"development",
entry:'./main.js',
output:{
path:__dirname,
filename:'build.js'
},
resolve: {
// if you don't write this,you can't use components to register component
//only use render to register component
alias: {
'vue': 'vue/dist/vue.js'
}
},
module:{
rules:[
{
test: /\.vue$/,
loader: "vue-loader"
},
{
test: /\.css$/,
loader: ["style-loader","css-loader"]
},
{
test: /\.js$/,
loader: "babel-loader",
exclude: /node_modules/
}
]
},
plugins: [
new VueLoaderPlugin()
]
}
如果是用less
或者stylus
或者scss
,我们还需要格外安装依赖,例如less
需要安装less-loader
,这里我们就只用style-loader与css-loader
即可,如对less
等感兴趣可自行研究。
特别说明:由于我们所编写的时间线组件并没有用到图标,所以无需添加图标以及图片的处理。
时间线组件可以分成三部分组成,第一部分即时间线,第二部分即时间戳,第三部分则是内容。我们先来看时间线组件的一个结构:
<timeline>
<timeline-item></timeline-item>
</timeline>
从上图我们可以看到时间线组件包含两个组件,即timeline
与timeline-item
组件,接下来我们来分析一下组件的属性有哪些。首先是父组件timeline
组件,根据element ui官方文档。我们可以看到父组件仅仅只提供了一个reverse
属性,即指定节点排序方向,默认为正序,但实际上我们还可以添加一个属性,那就是direction
属性,因为element ui
默认给的时间线组件只有垂直方向,而并没有水平方向,因此我们提供这个属性来确保时间线组件分为水平时间线和垂直时间线。
根据以上分析,我们总结如下:
direction:'vertical' //或'horizontal'
reverse:true //或false
接下来,我们来看子组件的属性,它包含时间戳,是否显示时间戳,时间戳位置,节点的类型,节点的图标,节点的颜色以及节点的尺寸
。这里我们暂时忽略图标这个选项。因此我们可以将属性定义如下:
timestamp:'2020/9/1' //时间戳内容
showTimestamp:true //或false,表示是否显示时间戳
timestampPlacement:'top' //或'bottom',即时间戳的显示位置
nodeColor:'#fff'//节点的颜色值
nodeSize:'size' //节点的尺寸
nodeIcon:'el-icon-more' //节点的图标,在这里我们没有引入element ui组件,因此不添加这个属性,如果要添加这个属性,需要先编写图标组件
确定了以上属性之后,我们就可以先来编写一个静态的组件元素结构,如下图所示:
<!-- 父组件结构 -->
<div class="timeline">
<!-- 子组件结构 -->
<div class="timeline-item">
<!-- 时间线 -->
<div class="timeline-item-tail"></div>
<!-- 时间线上的节点 -->
<div class="timeline-item-node"></div>
<!-- 时间线的时间戳与内容 -->
<div class="timeline-item-wrapper">
<!-- 时间戳,位置在top-->
<div class="timeline-item-timestamp is-top"></div>
<!-- 每一个时间戳对应的内容 -->
<div class="timeline-item-content"></div>
<!-- 时间戳,位置在bottom-->
<div class="timeline-item-timestamp is-bottom"></div>
</div>
</div>
</div>
根据以上代码,我们可以清晰的看到一个时间线的元素构成,为了确保布局方便,我们多写几个子元素,即time-line-item
以及它的所有子元素。接下来,我们开始编写静态的样式。如下:
.timeline {
font-size: 14px;
margin: 0;
background-color: #ffffff;
}
.timeline-item {
position: relative;
padding-bottom: 20px;
}
.timeline-item-tail {
position: absolute;
}
.is-vertical .timeline-item-tail {
border-left: 3px solid #bdbbbb;
height: 100%;
left: 3px;
}
.is-horizontal .timeline-item .timeline-item-tail {
width: 100%;
border-top: 3px solid #bdbbbb;
top: 5px;
}
.is-horizontal:after {
content: " ";
display: block;
height: 0;
visibility: hidden;
clear: both;
}
.timeline-item.timeline-item-info .timeline-item-tail {
border-color: #44444f;
}
.timeline-item.timeline-item-info .timeline-item-node {
background-color: #44444f;
}
.timeline-item.timeline-item-info .timeline-item-content {
color: #44444f;
}
.timeline-item.timeline-item-primary .timeline-item-tail {
border-color: #2396ef;
}
.timeline-item.timeline-item-primary .timeline-item-node {
background-color: #2396ef;
}
.timeline-item.timeline-item-primary .timeline-item-content {
color: #2396ef;
}
.timeline-item.timeline-item-success .timeline-item-tail {
border-color: #23ef3e;
}
.timeline-item.timeline-item-success .timeline-item-node {
background-color: #23ef3e;
}
.timeline-item.timeline-item-success .timeline-item-content {
color: #23ef3e;
}
.timeline-item.timeline-item-warning .timeline-item-tail {
border-color: #efae23;
}
.timeline-item.timeline-item-warning .timeline-item-node {
background-color: #efae23;
}
.timeline-item.timeline-item-warning .timeline-item-content {
color: #efae23;
}
.timeline-item.timeline-item-error .timeline-item-tail {
border-color: #ef5223;
}
.timeline-item.timeline-item-error .timeline-item-node {
background-color: #ef5223;
}
.timeline-item.timeline-item-error .timeline-item-content {
color: #ef5223;
}
.is-horizontal .timeline-item {
float: left;
}
.is-horizontal .timeline-item-wrapper {
padding-top: 18px;
left: -28px;
}
.timeline-item-node {
background-color: #e1e6e6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
}
.timeline-item-node-normal {
width: 12px;
height: 12px;
left: -2px;
}
.timeline-item-node-large {
width: 14px;
height: 14px;
left: -4px;
}
.timeline-item-wrapper {
position: relative;
top: -3px;
padding-left: 28px;
}
.timeline-item-content {
font-size: 12px;
color: #dddde0;
line-height: 1;
}
.timeline-item-timestamp {
color: #666;
}
.timeline-item-timestamp.is-top {
margin-bottom: 8px;
padding-top: 6px;
}
.timeline-item-timestamp.is-bottom {
margin-top: 8px;
}
.timeline-item:last-child .timeline-item-tail {
display: none;
}
接下来我们就要开始实现组件的逻辑封装了,首先我们需要封装timeline
组件,为了将该组件归纳到一个目录下,我们先新建一个目录,叫timeline
,然后新建一个index.vue
文件,并且将我们编写好的css
代码给移到该文件下,现在,你看到该文件的代码应该如下所示:
<script>
export default {
name: "timeline"
}
</script>
<style>
.timeline {
font-size: 14px;
margin: 0;
background-color: #ffffff;
}
.timeline-item {
position: relative;
padding-bottom: 20px;
}
.timeline-item-tail {
position: absolute;
}
.is-vertical .timeline-item-tail {
border-left: 3px solid #bdbbbb;
height: 100%;
left: 3px;
}
.is-horizontal .timeline-item .timeline-item-tail {
width: 100%;
border-top: 3px solid #bdbbbb;
top: 5px;
}
.is-horizontal:after {
content: " ";
display: block;
height: 0;
visibility: hidden;
clear: both;
}
.timeline-item.timeline-item-info .timeline-item-tail {
border-color: #44444f;
}
.timeline-item.timeline-item-info .timeline-item-node {
background-color: #44444f;
}
.timeline-item.timeline-item-info .timeline-item-content {
color: #44444f;
}
.timeline-item.timeline-item-primary .timeline-item-tail {
border-color: #2396ef;
}
.timeline-item.timeline-item-primary .timeline-item-node {
background-color: #2396ef;
}
.timeline-item.timeline-item-primary .timeline-item-content {
color: #2396ef;
}
.timeline-item.timeline-item-success .timeline-item-tail {
border-color: #23ef3e;
}
.timeline-item.timeline-item-success .timeline-item-node {
background-color: #23ef3e;
}
.timeline-item.timeline-item-success .timeline-item-content {
color: #23ef3e;
}
.timeline-item.timeline-item-warning .timeline-item-tail {
border-color: #efae23;
}
.timeline-item.timeline-item-warning .timeline-item-node {
background-color: #efae23;
}
.timeline-item.timeline-item-warning .timeline-item-content {
color: #efae23;
}
.timeline-item.timeline-item-error .timeline-item-tail {
border-color: #ef5223;
}
.timeline-item.timeline-item-error .timeline-item-node {
background-color: #ef5223;
}
.timeline-item.timeline-item-error .timeline-item-content {
color: #ef5223;
}
.is-horizontal .timeline-item {
float: left;
}
.is-horizontal .timeline-item-wrapper {
padding-top: 18px;
left: -28px;
}
.timeline-item-node {
background-color: #e1e6e6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
}
.timeline-item-node-normal {
width: 12px;
height: 12px;
left: -2px;
}
.timeline-item-node-large {
width: 14px;
height: 14px;
left: -4px;
}
.timeline-item-wrapper {
position: relative;
top: -3px;
padding-left: 28px;
}
.timeline-item-content {
font-size: 12px;
color: #dddde0;
line-height: 1;
}
.timeline-item-timestamp {
color: #666;
}
.timeline-item-timestamp.is-top {
margin-bottom: 8px;
padding-top: 6px;
}
.timeline-item-timestamp.is-bottom {
margin-top: 8px;
}
.timeline-item:last-child .timeline-item-tail {
display: none;
}
</style>
PS:加载sourceMap还需要这样一个配置devtool: 'inline-source-map'
为了确保父子组件共享状态,我们利用provide/inject API来传递this
对象,如下所示:
export default {
name: "timeline",
provide(){
return {
timeline:this
}
}
}
然后我们开始定义父组件的属性,根据前面所述,它包含两个属性,因此我们定义好在props
中,如下所示:
import { oneOf } from '../util'
export default {
name: "timeline",
provide(){
return {
timeline:this
}
},
props:{
reverse:{
type:Boolean,
default:false
},
direction:{
type:String,
default:'vertical',
validator:(value) => {
return oneOf(['vertical','horizontal'],value,'vertical');
}
}
}
}
上面代码需要用到一个工具函数oneOf
,顾名思义,就是必须是其中的一项,它有三个参数,第一个参数是匹配的数组,第二个参数是匹配的项,第三个是提供的默认项,该工具函数代码如下:
export const oneOf = (arr,value,defaultValue) => {
return arr.reduce((r,i) => i === value ? i : r,defaultValue);
}
其实也不难理解,就是我们想要的值必须是数组的一项,如果不是就返回默认项,工具函数内部代码,我们可以写得更清晰明了一点,如下所示:
export const oneOf = (arr,value,defaultValue) => {
return arr.reduce((result,item) => {
return item === value ? item : value;
},defaultValue);
}
以上的代码经过简洁处理就得到了前面的一行代码,如果理解不了,可以采用后者的代码,至于validator
验证选项,可参考vue-prop-validator-自定义验证函数。
接下来,我们在render
方法中去渲染这个父组件,代码如下:
import { oneOf } from '../util'
export default {
name: "timeline",
provide(){
return {
timeline:this
}
},
props:{
reverse:{
type:Boolean,
default:false
},
direction:{
type:String,
default:'vertical',
validator:(value) => {
return oneOf(['vertical','horizontal'],value,'vertical');
}
}
},
//新添加的内容
render(){
const reverse = this.reverse;
const direction = this.direction;
const classes = {
'timeline':true,
'is-reverse':reverse,
['is' + direction]:true
}
const slots = this.$slots.default || [];
if(reverse)slots = slots.reverse();
return (<div class={classes}>{slots}</div>)
}
}
以上代码似乎不是很好理解,其实也不难理解,首先是获取reverse
和direction
属性,接着就是设置类名对象,类型对象包含三个,然后就是获取该组件的默认插槽内容,判断如果提供了reverse
属性,则调用数组的reverse
方法来让slots
倒序,在这里我们应该很清晰的明白 slots
如果存在,那么则一定是一个vNode
节点组成的数组,如果没有,默认就是一个空数组。然后最后返回一个父元素包含该插槽的jsx
元素。
在这里,我们使用了jsx
语法,而我们的项目环境当中还并没有添加处理jsx
的依赖,所以我们需要再次安装处理jsx
语法的依赖babel-plugin-syntax-jsx babel-plugin-transform-vue-jsx
,并添加相应的配置。继续在终端输入以下命令安装依赖:
npm install babel-plugin-syntax-jsx babel-plugin-transform-vue-jsx --save-dev
然后在babel.config.json
中添加一行配置代码如下:
"plugins": ["transform-vue-jsx"]
接着在webpack.config.js
中添加一行配置处理如下:
{
test: /\.(jsx?|babel|es6|js)$/,
loader: 'babel-loader',
exclude: /node_modules/
}
现在,我们可以看到页面中不会报错,未处理jsx
了。
到此为止,父组件的逻辑代码也就完成了,接下来,让我们在全局里面使用一下该组件,看看是否生效。
在main.js
里面添加如下一行代码:
import Timeline from './components/timeline/timeline.vue'
Vue.component(Timeline.name,Timeline)
然后在App.vue
里面,我们使用这个组件,代码如下:
<template>
<div id="app">
<timeline></timeline>
</div>
</template>
ok,似乎看起来没什么问题,让我们继续编写子组件的逻辑代码。
接下来,新建一个item.vue
文件,在该文件中编写如下代码:
<template>
<div class="timeline-item">
<div class="timeline-item-tail"></div>
<div class="timeline-item-node"
:class="[`timeline-item-node-${ size || ''}`,`timeline-item-node-${type || ''}`]"
:style="{ backgroundColor:color }"
v-if="!$slots.dot"
></div>
<div class="timeline-item-node" v-if="$slots.dot">
<slot name="dot"></slot>
</div>
<div class="timeline-item-wrapper">
<div class="timeline-item-timestamp"
:class="[`is-`+ timestampPlacement ]"
v-if="item.placement === 'top' && showTimestamp"
>{{ timestamp }}</div>
<div class="timeline-item-content"><slot></slot></div>
<div class="timeline-item-timestamp"
:class="[`is-`+ timestampPlacement ]"
v-if="item.placement === 'bottom' && showTimestamp"
>{{ timestamp }}</div>
</div>
</div>
</template>
<script>
export default {
name:"timeline-item",
inject:['timeline'],
props:{
timestamp:String,
showTimestamp:{
type:Boolean,
default:false
},
timestampPlacement:{
type:String,
default:'top',
validator:(value) => {
return oneOf(['top','bottom'],value,'bottom')
}
},
type:{
type:String,
default:'default',
validator:(value) => {
return oneOf(['default','info','primary','success','warning','error'],value,'default');
}
},
size:{
type:String,
default:'normal',
validator:(value) => {
return oneOf(['normal','large'],value,'normal')
}
},
color:String
}
}
</script>
编写完成之后,让我们继续在main.js
中引用它,然后在App.vue
中使用它。代码分别如下:
//main.js
import TimelineItem from './components/timeline/timeline-item.vue'
Vue.component(TimelineItem.name,TimelineItem)
<!--App.vue-->
<timeline>
<timeline-item type="default" size="large" :show-timestamp="true" timestamp="2020/9/6" timestamp-placement="top">待审核</timeline-item>
<timeline-item type="info" size="normal" :show-timestamp="true" timestamp="2020/9/6" timestamp-placement="bottom">审核中</timeline-item>
<timeline-item type="error" size="large" :show-timestamp="true" timestamp="2020/9/6" timestamp-placement="top">审核失败</timeline-item>
<timeline-item type="success" size="normal" :show-timestamp="true" timestamp="2020/9/6" timestamp-placement="bottom">审核完成</timeline-item>
</timeline>
接下来我们就可以看到一个时间线已经完美的展示了,时间线组件算是大功告成了。
本文档已经录制为视频,地址如下:
源码和文档已经上传到github。
查看原文赞 5 收藏 4 评论 0
夕水 回答了问题 · 2020-08-13
function() {
console.log(this);
}
//函数会有作用域,可以使用箭头函数
() => { console.log(this); }
关注 4 回答 3
夕水 回答了问题 · 2020-08-11
let cn = json.reduce((r,i) => (r[i.permission] = i['title'],r),{});
关注 4 回答 3
夕水 回答了问题 · 2020-08-07
可以使用postMessage api将事件发送,然后另一边接收。
关注 3 回答 3
夕水 发布了文章 · 2020-07-14
前一阵子看到了大火的github
项目vanillawebprojects,对其中的实现有些好奇,于是研究了写法,但是对英文有些不太熟悉的我,无法忍受这样的纯英文项目,于是花了二周时间,潜心研究了项目的实现思路,然后自己实现了这20
个web
项目的中文版。虽然思路来源于原项目,但是很多功能我做了扩展,相信细心的人一定会发现有哪些不同之处,让我们来一一看一下吧。
效果如下图所示:
效果如下图所示:
效果如下图所示:
效果如下图所示:
效果如下图所示:
效果如下图所示:
效果如下图所示:
效果如下图所示:
效果如下图所示:
效果如下图所示:
效果如下图所示:
效果如下图所示:
效果如下图所示:
效果如下图所示:
效果如下图所示:
效果如下图所示:
效果如下图所示:
效果如下图所示:
效果如下图所示:
效果如下图所示:
如果有人能发现bug
,欢迎提issue
,如果这些项目能够帮助到您,希望您能点个star
。
赞 31 收藏 23 评论 0
夕水 发布了文章 · 2020-07-10
提起数据可视化技术,都不免会让人想到echarts,而最简单的入门图表就是柱状图了。它是基于canvas
来实现的,而我在想,如果不用canvas
,我是否能不用canvas
实现柱状图,经过我的探索,终于实现了一款柱状图表。
让我们查看一个已经完成的在线示例,如下图所示:
首先,我们需要确定柱状图表有哪些部分,第一右上角头部有legend
部分,第二有x
与y
轴部分,第三就是柱状图部分了。好了确定了有哪些部分,我们就可以很好的实现了,好了,让我们进入正题吧。
目前我们完成的成品是已经封装好的,然后页面就只有一个容器元素。但我们最开始肯定不能这样写,我们先写一个写死的结构如下所示:
<div id="weekCost" class="ew-charts">
<ew-charts-body>
<ew-charts-legend>
<i class="leg-1"></i>
<span>直接访问</span>
<i class="leg-2"></i>
<span>邮件营销</span>
<i class="leg-3"></i>
<span>联盟广告</span>
<i class="leg-4"></i>
<span>视频广告</span>
<i class="leg-5"></i>
<span>搜索引擎</span>
</ew-charts-legend>
<ew-charts-x>
<div class="x-1" style="letter-spacing:2px;">一月</div>
<div class="x-2" style="letter-spacing:2px;">二月</div>
<div class="x-3" style="letter-spacing:2px;">三月</div>
<div class="x-4" style="letter-spacing:2px;">四月</div>
<div class="x-5" style="letter-spacing:2px;">五月</div>
<div class="x-6" style="letter-spacing:2px;">六月</div>
<div class="x-7" style="letter-spacing:2px;">七月</div>
</ew-charts-x>
<ew-charts-y>
<div class="y-1">500</div>
<div class="y-2">1000</div>
<div class="y-3">1500</div>
<div class="y-4">2000</div>
</ew-charts-y>
<ew-charts-zone>
<div class="zone-1">
<bar class="bar-1 dataId-1-1" data-value="320"></bar>
<bar class="bar-2 dataId-1-2" data-value="120"></bar>
<bar class="bar-3 dataId-1-3" data-value="220"></bar>
<bar class="bar-4 dataId-1-4" data-value="150"></bar>
<bar class="bar-5 dataId-1-5" data-value="862"></bar>
</div>
<div class="zone-2">
<bar class="bar-1 dataId-2-1" data-value="332"></bar>
<bar class="bar-2 dataId-2-2" data-value="132"></bar>
<bar class="bar-3 dataId-2-3" data-value="182"></bar>
<bar class="bar-4 dataId-2-4" data-value="232"></bar>
<bar class="bar-5 dataId-2-5" data-value="1018"></bar>
</div>
<div class="zone-3">
<bar class="bar-1 dataId-3-1" data-value="301"></bar>
<bar class="bar-2 dataId-3-2" data-value="101"></bar>
<bar class="bar-3 dataId-3-3" data-value="191"></bar>
<bar class="bar-4 dataId-3-4" data-value="201"></bar>
<bar class="bar-5 dataId-3-5" data-value="964"></bar>
</div>
<div class="zone-4">
<bar class="bar-1 dataId-4-1" data-value="334"></bar>
<bar class="bar-2 dataId-4-2" data-value="134"></bar>
<bar class="bar-3 dataId-4-3" data-value="234"></bar>
<bar class="bar-4 dataId-4-4" data-value="154"></bar>
<bar class="bar-5 dataId-4-5" data-value="1026"></bar>
</div>
<div class="zone-5">
<bar class="bar-1 dataId-5-1" data-value="390"></bar>
<bar class="bar-2 dataId-5-2" data-value="90"></bar>
<bar class="bar-3 dataId-5-3" data-value="290"></bar>
<bar class="bar-4 dataId-5-4" data-value="190"></bar>
<bar class="bar-5 dataId-5-5" data-value="1679"></bar>
</div>
<div class="zone-6">
<bar class="bar-1 dataId-6-1" data-value="330"></bar>
<bar class="bar-2 dataId-6-2" data-value="230"></bar>
<bar class="bar-3 dataId-6-3" data-value="330"></bar>
<bar class="bar-4 dataId-6-4" data-value="330"></bar>
<bar class="bar-5 dataId-6-5" data-value="1600"></bar>
</div>
<div class="zone-7">
<bar class="bar-1 dataId-7-1" data-value="320"></bar>
<bar class="bar-2 dataId-7-2" data-value="210"></bar>
<bar class="bar-3 dataId-7-3" data-value="310"></bar>
<bar class="bar-4 dataId-7-4" data-value="410"></bar>
<bar class="bar-5 dataId-7-5" data-value="1570"></bar>
</div>
</ew-charts-zone>
</ew-charts-body>
</div>
接下来就需要根据页面元素,一个一个的添加样式了,这是一个慢工细活的过程,需要慢慢来。
/**
* 功能:普通页面样式设置
**/
/*********************************************/
/* 样式初始化部分 */
/*********************************************/
* {
margin: 0;
padding: 0;
}
body,html {
height: 100%;
font: 20px "微软雅黑";
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
overflow: hidden;
}
/* 转换为IE盒子模型 */
*,*::before,*::after {
box-sizing: border-box;
}
/* 手型按钮 */
button,
input[type="button"],
input[type="submit"],
input[type="reset"],
input[type="radio"],
input[type="checkbox"],
a {
cursor: pointer;
}
button,
input,
textarea,
select {
outline: none;
}
@charset "utf-8";
/**
* 功能:统计图表样式
**/
/**** 图表自定义标签初始化部分 ****/
.ew-charts,
ew-charts-body,
ew-charts-x,
ew-charts-y,
ew-charts-zone,
ew-charts-legend {
display: block;
}
ew-charts-x,
ew-charts-x>div,
ew-charts-y,
ew-charts-y>div {
box-sizing: border-box;
position: absolute;
overflow: hidden;
}
ew-charts-zone,
ew-charts-zone>div,
ew-charts-zone>div bar {
box-sizing: border-box;
}
ew-charts-body,
ew-charts-zone>div,
ew-charts-zone>div bar {
position: relative;
}
ew-charts-zone,
ew-charts-zone>div bar,
ew-charts-legend,
ew-charts-zone>div bar>span {
position: absolute;
}
/* 图表容器 */
.ew-charts {
width: 100%;
height: 100%;
color: #f8f5fa;
background: linear-gradient(to right, #234, #789);
margin: auto;
color: #b3b3b3;
}
/*表体*/
ew-charts-body {
width: 100%;
height: 100%;
font-size: 16px;
}
/*X轴*/
ew-charts-x {
width: 90%;
height: 8%;
border-top: 1px solid #fefefe;
left: 6%;
bottom: 0;
}
ew-charts-x>div {
height: 100%;
text-align: center;
line-height: 30px;
top: 0;
}
/*Y轴*/
ew-charts-y {
width: 6%;
height: 80%;
border-right: 1px solid #fefefe;
overflow: visible;
left: 0;
top: 12%;
}
ew-charts-y>div {
width: 100%;
height: 24px;
text-align: right;
padding-right: 6px;
left: 0;
}
/*表格数据区间*/
ew-charts-zone {
width: 90%;
height: 80%;
left: 6%;
top: 12%;
}
ew-charts-zone>div {
height: 100%;
float: right;
}
ew-charts-zone>div bar {
height: 0;
bottom: 0;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
text-shadow: 0 0 5px rgba(255, 255, 255, 0.8);
transition: 0.6s cubic-bezier(.19, .55, .58, 1.3);
/*默认值设置*/
background-color: #606060;
border: 1px solid #cdcdcd;
box-shadow: 0 0 5px #606060;
}
ew-charts-zone>div bar:hover {
z-index: 10;
}
ew-charts-zone>div bar>span {
left: 50%;
top: -40px;
transform: translateX(-50%);
font: 32px "方正姚体", "arial";
opacity: 0;
}
ew-charts-zone>div bar>span.animation {
animation: data-value-show 0.6s forwards;
}
/*图注*/
ew-charts-legend {
top: 10px;
right: 4%;
}
ew-charts-legend i,
ew-charts-legend span {
display: inline-block;
vertical-align: middle;
}
ew-charts-legend i {
width: 34px;
height: 20px;
border-radius: 3px;
margin-left: 12px;
margin-right: 6px;
background-color: #606060;
border: 1px solid #cdcdcd;
}
ew-charts-legend span {
letter-spacing: 2px;
}
/*图表动画部分*/
@keyframes data-value-show {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
首先,我们需要定义一个函数,用于封装。
function ewCharts(options) {
//这里是判断传入的参数中是否含有color属性,从而给予color属性值
if (!Array.isArray(options.color) || options.color.length !== options.data.Y.length) {
let len = options.data.Y.length - options.color.length;
for (let i = 0; i < len; i++) {
options.color.push('#ffffff');
}
}
//为后期的扩展做准备,type类型为bar就是默认的柱状图
options.type = options.type === "bar" ? options.type : "bar";
//将参数赋值到实例上
this.options = options;
//开始初始化
this.init(options);
}
接着,我们可以看到页面效果颜色有点高亮,接下来就是完成颜色的高亮效果工具函数,如下所示:
/**
* 颜色高亮
*/
ewCharts.prototype.lightColor = function (color) {
// 传入的颜色为16进制颜色模式,如:#ffffff
let everyColorLight = function (lightColor) {
// 将传入的颜色转换成16进制数字,然后再乘以1.6相当于将颜色变亮1.6倍
const value = Math.round(parseInt(lightColor, 16) * 1.6);
// 值有一个最小值与最大值,当超过255则等于255,最小值不能小于16
return (value >= 255 ? 255 : value <= 16 ? 16 : value).toString(16);
}
// 相当于处理每一区间的颜色代码,除了#之外的,每2位代表一种颜色,如#fef2f3,则f2代表红色区间,f2代表绿色区间,f3代表蓝色区间
return '#' + everyColorLight(color.slice(1, 3)) + everyColorLight(color.slice(3, 5)) + everyColorLight(color.slice(5, 7));
}
然后,我们需要创建一个设置样式的函数,如下所示:
/**
* 样式规则设置
*/
ewCharts.prototype.setStyle = function () {
//这里的操作无非就是判断页面中是否含有link标签,如果含有,就将样式规则插入到该标签所包含的样式表中
let link = this.$('link', false), linkIndex = 0;
for (let i = 0, len = link.length; i < len; i++) {
if (/\w+\.css/.test(link[i].getAttribute('href'))) {
linkIndex = i;
}
}
//api文档https://www.w3school.com.cn/xmldom/met_cssstylesheet_insertrule.asp
return link[linkIndex].sheet.insertRule.bind(link[linkIndex].sheet);
}
然后,我们再来完成一个获取DOM
元素的函数封装,如下所示:
/**,
* 获取DOM元素
*/
ewCharts.prototype.$ = function (selector, isSingle) {
// 如果传入的包含#,则是唯一的元素执行querySelector方法,否则根据传入的布尔值来判断执行哪个方法查询DOM
isSingle = selector.indexOf('#') > -1 ? true : typeof isSingle === 'boolean' ? isSingle : true;
return isSingle ? document.querySelector(selector) : document.querySelectorAll(selector);
}
然后,我们就来完成初始化函数,如下所示:
/**
* 初始化
*/
ewCharts.prototype.init = function (options) {
// 设置样式规则
let setStyle = this.setStyle();
//图表类型判断,为后期做扩展
switch (options.type) {
case "bar":
//初始化页面图表所有部分
this.resetAllCharts(this.$(options.el));
//初始化X轴部分
this.resetChartsX(options.data.X, setStyle);
//初始化Y轴部分
this.resetChartsY(options.data.Y, setStyle);
//初始化图注部分
this.resetChartsLegend(options.data, setStyle);
break;
}
}
然后,完成初始化页面图表的结构,前面页面结构和css
写好只有,页面应该只保留一个容器元素,如下所示:
<div id="weekCost"></div>
接下来,我们就往该元素添加结构,如下所示:
/**
* 初始化图表结构
*/
ewCharts.prototype.resetAllCharts = function (el) {
el.innerHTML = "<ew-charts-body>" +
"<ew-charts-legend></ew-charts-legend>" +
"<ew-charts-x></ew-charts-x>" +
"<ew-charts-y></ew-charts-y>" +
"<ew-charts-zone></ew-charts-zone>" +
"</ew-charts-body>";
//为容器元素添加一个类名
el.classList.add('ew-charts');
return el;
}
继续初始化X
轴,如下所示:
/**
* 设置X轴
* x轴的数据
* 设置样式的方法
*/
ewCharts.prototype.resetChartsX = function (dataX, setStyle) {
let chartsX = this.$('ew-charts-x'), chartsXHTML = '';
let dataXLen = dataX.length;
// 添加x轴的文本元素
for (let i = 0; i < dataXLen; i++) {
chartsXHTML += "<div class=x-" + (i + 1) + " style='letter-spacing:2px;'>" + dataX[i] + "</div>";
}
chartsX.innerHTML = chartsXHTML;
let chartsXContent = this.$('ew-charts-x > div', false), chartsXContentWidthArr = [];
// 获取元素的宽度数组,并找到最大宽度,从而设置每个元素的宽度为最大宽度
for (let j = 0; j < dataXLen; j++) {
chartsXContentWidthArr.push(chartsXContent[j].offsetWidth);
}
//最大宽度与单位宽度以及单位宽度的一半
let maxWidth = Math.max.apply(null, chartsXContentWidthArr), unitWidth = parseInt(100 / dataXLen), half = unitWidth / 2;
for (let k = 0; k < dataXLen; k++) {
//循环分别设置x轴上的坐标数据的元素宽度与left偏移量
setStyle('ew-charts-x > div.x-' + (k + 1) + '{width:' + maxWidth + 'px;' + 'left:calc(' + (unitWidth * (k + 1) - half) + '% - ' + half + 'px)}', k);
}
}
x
轴部分已经完成,继续完成y
轴部分:
/**
* 设置Y轴
*/
ewCharts.prototype.resetChartsY = function (dataY, setStyle) {
let newDataValue = [], chartsY = this.$('ew-charts-y'), chartsYHTML = '';
let keyNameArr = this.options.data.keyName;
let keyValue = Array.isArray(keyNameArr) && keyNameArr.length === 2 ? keyNameArr[1] : 'value';
for (let i = 0, len = dataY.length; i < len; i++) {
// 将多个value值数组合并成一个数组
newDataValue = newDataValue.concat(dataY[i][keyValue]);
}
// 求value数组的最大值
let maxValue = Math.max.apply(null, newDataValue);
if (/\./.test(String(maxValue))) {
// 如果最大值有小数,则向上取整
maxValue = Math.ceil(maxValue);
}
// 定义分段数与当前Y轴的最大值
let subSections = null, currentMaxValue = null;
// 按照每段为1,5,50,500,5000,50000基准值来分段的
// 当前作为基准值判断的依据数组
let judgeMaxArr = [1000000, 100000, 10000, 1000, 100, 10];
let currentJudgeValue = null;
for (let l = 0, length = judgeMaxArr.length; l < length; l++) {
// 如果满足条件就跳出循环
if (maxValue >= judgeMaxArr[l]) {
currentJudgeValue = judgeMaxArr[l];
break;
}
}
// 如果currentValue的值为null,则默认分段值设为1
if (!currentJudgeValue) currentJudgeValue = 1;
// 计算分段数
subSections = currentJudgeValue > 1 ? Math.ceil(maxValue / (currentJudgeValue / 2)) : Math.ceil(maxValue / currentJudgeValue);
// 计算当前Y轴最大值
currentMaxValue = currentJudgeValue > 1 ? subSections * (currentJudgeValue / 2) : subSections * currentJudgeValue;
// 根据分段数来生成Y轴元素
for (let j = 0; j < subSections; j++) {
chartsYHTML += "<div class='y-" + (j + 1) + "'>" + (currentMaxValue / subSections) * (j + 1) + "</div>";
}
chartsY.innerHTML = chartsYHTML;
// 设置CSS规则
for (let k = 0; k < subSections; k++) {
setStyle('ew-charts-y > div.y-' + (k + 1) + '{ bottom:calc(' + parseInt((100 / subSections) * (k + 1)) + '% - 16px);}');
}
// 设置区域
this.resetChartsZone(subSections, keyValue, currentMaxValue, setStyle);
}
y
轴部分也已经完成,接下来是完成柱状图部分,也就是区域部分,如下:
/**
* 设置区域
*/
ewCharts.prototype.resetChartsZone = function (subSections, keyValue, currentMaxValue, setStyle) {
// 区域整体背景
setStyle("ew-charts-zone { background:repeating-linear-gradient(180deg,#535456 0%,#724109 " + 100 / subSections + "%,#334455 calc(" + 100 / subSections + "% + 1px),#e0e1e5 " + 100 / subSections * 2 + "%)}", subSections + 1);
let zoneLen = this.options.data.X.length;
let chartsZone = this.$('ew-charts-zone'), chartsZoneHTML = '';
// 因为设置了margin-left与margin-right各1%,所以要减去2
let series_unit = parseInt(100 / zoneLen) - 2;
// 设置剩余空间
let freeSpace = 0;
// 系列数
let series_count = this.options.data.Y.length;
// 每一条数据的宽度
let series_width = 0;
// 每一条数据的left值
let series_left = null;
// 根据系列数来调整样式
if (series_count < 3) {
series_width = 28;
freeSpace = (100 - (series_count * 30)) / 2;
series_left = 30;
} else if (series_count >= 3 && series_count < 6) {
series_width = 18;
freeSpace = (100 - (series_count * 20)) / 2;
series_left = 20;
} else {
series_width = 100 / (series_count - 1);
freeSpace = 100 / series_count;
series_left = 0;
}
let seriesHTML = '';
for (let j = 0; j < series_count; j++) {
// 边框颜色高亮
let borderColor = this.lightColor(this.options.color[j]);
let left = null;
if (series_left > 0) {
left = series_left * j + freeSpace;
} else {
left = freeSpace * j;
}
// 设置初始样式
setStyle('ew-charts-zone > div bar.bar-' + (j + 1) + "{width:" + series_width + '%;background-color:' + this.options.color[j] + ';border-color:' + borderColor + ';left:' + left + '%;box-shadow:0 0 5px ' + this.options.color[j] + ';}', j);
// 设置悬浮样式
setStyle('ew-charts-zone > div bar.bar-' + (j + 1) + ':hover{box-shadow:0 0 15px ' + this.options.color[j] + ';}');
seriesHTML += '<bar class="bar-' + (j + 1) + '"></bar>'
}
setStyle("ew-charts-zone > div[class*='zone-']{ width:" + series_unit + "%;margin-left:1%;margin-right:1%;}");
for (let i = 0; i < zoneLen; i++) {
chartsZoneHTML += "<div class='zone-" + (i + 1) + "'>" + seriesHTML + "</div>";
}
chartsZone.innerHTML = chartsZoneHTML;
let dataY = this.options.data.Y;
// 延迟设置高度
setTimeout(() => {
for (let k = 0; k < zoneLen; k++) {
for (let l = 0; l < series_count; l++) {
// 获取bar元素
const bar = chartsZone.children[k].children[l];
// 设置class类名,方便设置样式规则
bar.classList.add('dataId-' + (k + 1) + '-' + (l + 1));
// 设置值,方便后续的悬浮操作显示值
bar.setAttribute('data-value', dataY[l][keyValue][k]);
// 设置高度
setStyle('ew-charts-zone > div bar.dataId-' + (k + 1) + '-' + (l + 1) + '{height:' + (dataY[l][keyValue][k]) / currentMaxValue * 100 + '%;}', l);
}
}
// 绑定悬浮事件
let bar = this.$('ew-charts-zone div bar', false);
[].slice.call(bar).forEach((item) => {
item.onmouseenter = function () {
let value = this.getAttribute('data-value');
this.innerHTML = "<span class='animation'>" + value + '</span>';
}
item.onmouseleave = function () {
this.innerHTML = '';
}
})
}, 0);
}
最后就是完成图注部分了,如下所示:
/**
* 设置图注
*/
ewCharts.prototype.resetChartsLegend = function (dataLegend, setStyle) {
let legendHTML = "";
//图注数据的属性名
let keyName = Array.isArray(dataLegend.keyName) && dataLegend.keyName.length === 2 ? dataLegend.keyName[0] : 'label';
for (let i = 0, len = dataLegend.Y.length; i < len; i++) {
let borderColor = this.lightColor(this.options.color[i]);
setStyle("ew-charts-legend > i.leg-" + (i + 1) + "{ background:" + this.options.color[i] + ";border-color:" + borderColor + ";}", i);
legendHTML += "<i class='leg-" + (i + 1) + "'></i><span>" + dataLegend.Y[i][keyName] + "</span>";
}
this.$('ew-charts-legend').innerHTML = legendHTML;
}
接下来,调用这个封装好的函数,如下所示:
/**
* 功能:调用统计图表功能
**/
/************************************************/
/* DOM加载完毕后执行(多媒体资源尚未开始加载) */
/************************************************/
document.onreadystatechange = function(){
if(document.readyState == "interactive"){
let ewChart = new ewCharts({
el:"#weekCost",
color:["#07bc85","dd2345","#346578","#ff8654","#998213"],
data:{
X:['一月', '二月', '三月', '四月', '五月', '六月', '七月'],
Y:[
{
name: '直接访问',
data: [320, 332, 301, 334, 390, 330, 320]
},
{
name: '邮件营销',
data: [120, 132, 101, 134, 90, 230, 210]
},
{
name: '联盟广告',
data: [220, 182, 191, 234, 290, 330, 310]
},
{
name: '视频广告',
data: [150, 232, 201, 154, 190, 330, 410]
},
{
name: '搜索引擎',
data: [862, 1018, 964, 1026, 1679, 1600, 1570]
},
],
keyName:['name','data']
}
});
console.log(ewChart);
}
}
嗯,一款柱状图表就大功告成了,由于每一部分的功能我都做了注释,所以不需要做详解,如有问题欢迎联系我,如果发现bug
,也欢迎提issue
,项目地址为:my-web-projects,如有帮助,望不吝啬star
。
赞 1 收藏 1 评论 0
夕水 回答了问题 · 2020-06-19
转换成时间戳的思路很好,以下是我的写法:
/**
* 将时间转换成时间戳,仅小时与分钟的转换,如7:03,21:00之类的时间
* @param {*} value
*/
let filterTime = function (value) {
const hour = Number(value.slice(0, value.indexOf(':')).replace(/^0/, '')) * 60 * 60 * 1000;
const minute = Number(value.slice(value.indexOf(':') + 1, value.length).replace(/^0/, '')) * 60 * 1000;
return hour + minute;
}
/**
* 判断时间段是否交叉
*/
let judgeDate = function (timeArr,startTime,endTime) {
for (let i = 0, len = timeArr.length; i < len; i++) {
let start = timeArr[i], end = timeArr[i];
//满足可以修改的时间段只有两种情况,即开始时间和结束时间都小于等于已知时间段与开始时间大于已知时间段的结束时间,但结束时间需要小于等于
//已知时间段的开始时间
if (filterTime(startTime) <= filterTime(start) && filterTime(endTime) <= filterTime(start)) {
return true;
} else if (filterTime(startTime) >= filterTime(end)) {
return true;
}
}
return false;
}
if(!judgeDate()){
console.log('时间交叉')
}
关注 4 回答 4
夕水 回答了问题 · 2020-06-17
文档中已经说明,Rust
中的数组是固定长度的,不允许改变长度,因此,你只能使用vector
来替代。中文站点你可以参考rust中文文档。
这里给你找到了一个优雅的写法:
fn parse_core_args(args: Vec<String>) -> (Vec<String>, Vec<String>) {
let mut rest = vec![];
// Filter out args that shouldn't be passed to V8
let mut args: Vec<String> = args
.into_iter()
.filter(|arg| {
if arg.as_str() == "--help" {
rest.push(arg.clone());
return false;
}
true
})
.collect();
// Replace args being sent to V8
for idx in 0..args.len() {
if args[idx] == "--v8-options" {
mem::swap(args.get_mut(idx).unwrap(), &mut String::from("--help"));
}
}
(args, rest)
}
关注 2 回答 1
夕水 回答了问题 · 2020-06-15
猜测你的传入echarts.init
的dom
元素参数有问题,从这方面上去调试,可以找到问题所在。
关注 3 回答 2
夕水 回答了问题 · 2020-05-12
document.querySelector('.a').firstElementChild
关注 2 回答 1
夕水 发布了文章 · 2020-05-10
消息提示框在实际应用场景当中比较常见,最常用的就是element ui的消息提示框,我们通常都是直接使用它们,但是我们有没有尝试过去探究其实现原理,并自己动手实现呢?为了提升我们的个人能力和竞争力,我们可以尝试来实现这样一个消息提示框。
我们来查看一下最终实现效果,如下图所示:
我们创建一个message文件夹,然后创建一个index.html文件,以及message.js和message.css文件,如下所示:
在html文件中,我们可以先来实现一个静态的消息提示框,代码如下:
<div class="message">
<p>这是一个提示框</p>
<i class="message-close-btn">×</i>
</div>
然后再message.css我们写上基本的css代码:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* 消息提示框容器样式 */
.message {
position: fixed;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
min-width: 300px;
background-color: #edf2fc;
border: 1px solid #edf2fc;
padding: 16px 17px;
top: 25px;
border-radius: 6px;
overflow: hidden;
z-index: 1000;
}
/* 关闭按钮样式 */
.message > .message-close-btn {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
color: #c0c0c4;
font-size: 17px;
cursor: pointer;
}
.message > .message-close-btn:hover,.message > .message-close-btn:active {
color: #909399;
}
/* 消息提示框内容样式 */
.message p {
line-height: 1;
font-size:14px;
color: #909399;
}
有四种提示框,以及让内容居中,我们不外乎是多加一个类名来写css样式,比如内容居中,我们只需要在html消息提示框容器元素加上一个类名,代码如下:
<div class="message message-center">
<p>这是一个提示框</p>
<i class="message-close-btn">×</i>
</div>
然后再css文件中加一段如下的css代码即可:
/* 内容居中 */
.message.message-center {
justify-content: center;
}
四种类型的提示框不外乎也是同样的原理,增加一个类名,然后改变的是背景色和字体色,所以html代码如下:
<div class="message message-success">
<p>这是一个成功提示框</p>
<i class="message-close-btn">×</i>
</div>
<div class="message message-warning">
<p>这是一个警告提示框</p>
<i class="message-close-btn">×</i>
</div>
<div class="message message-error">
<p>这是一个错误提示框</p>
<i class="message-close-btn">×</i>
</div>
css代码如下:
/* 成功提示框样式 */
.message.message-success {
background-color: #e1f3d8;
border-color:#e1f3d8;
}
.message.message-success p {
color: #67c23a;
}
/* 警告提示框样式 */
.message.message-warning{
background-color: #fdfce6;
border-color: #fdfce6;
}
.message.message-warning p{
color: #e6a23c;
}
/* 错误提示框样式 */
.message.message-error {
background-color: #fef0f0;
border-color: #fef0f0;
}
.message.message-error p {
color: #f56c6c;
}
这样一来,准备工作就完成了,接下来就是我们的重头戏,JavaScript代码,尝试将如上代码注释掉。
我们通过定义一个对象来表示消息提示框的类型,如下所示:
// 消息提示框的四种类型
let typeMap = {
info: "info",
warning: "warning",
success: "success",
error: "error"
}
我们分析一下需要传入的配置项有内容(content),关闭提示框时间(closeTime),是否显示关闭提示框按钮(showClose),内容居中(center)以及消息提示框类型(type)。所以定义配置项如下:
// 提示框的默认配置项
let messageOption = {
type: "info",
closeTime: 600,
center: false,
showClose: false,
content: "默认内容"
}
我们通过面向对象的编程思维将消息提示框当做是一个类对象,所以我们只需要创建一个类。虽然可以使用es6的class语法来创建,但是为了方便,我们使用构造函数来实现。创建一个构造函数Message,如下所示:
function Message(option) {
//这里做了一次初始化
this.init(option);
}
创建了消息提示框构造函数之后,我们需要传入配置项,并且我们在函数里做了初始化操作,接下来我们来实现初始化的操作。
Message.prototype.init = function (option) {
//这里创建了提示框元素,并将整个提示框容器元素添加到页面中
document.body.appendChild(this.create(option));
//这里设置提示框的top
this.setTop(document.querySelectorAll('.message'));
//判断如果传入的closeTime大于0,则默认关闭提示框
if (option.closeTime > 0) {
this.close(option.container, option.closeTime);
}
//点击关闭按钮关闭提示框
if (option.close) {
option.close.onclick = (e) => {
this.close(e.currentTarget.parentElement, 0);
}
}
}
在前面的初始化操作中,我们做了几个功能,首先创建提示框容器元素,并将提示框容器元素添加到页面bod中。我们还做了动态计算提示框的top以及判断传入的默认关闭时间来关闭提示框,点击关闭按钮关闭提示框。我们来看创建提示框的方法,即create方法的编写操作。如下:
Message.prototype.create = function (option) {
//这里做了一个判断,表示如果设置showClose为false即不显示关闭按钮并且closeTime也为0,即无自动关闭提示框,我们就显示关闭按钮
if(!option.showClose && option.closeTime <=0)option.showClose = true;
//创建容器元素
let element = document.createElement('div');
//设置类名
element.className = `message message-${option.type}`;
if (option.center) element.classList.add('message-center');
//创建关闭按钮元素以及设置类名和内容
let closeBtn = document.createElement('i');
closeBtn.className = 'message-close-btn';
closeBtn.innerHTML = '×';
//创建内容元素
let contentElement = document.createElement('p');
contentElement.innerHTML = option.content;
//判断如果显示关闭按钮,则将关闭按钮元素添加到提示框容器元素中
if (closeBtn && option.showClose) element.appendChild(closeBtn);
//将内容元素添加到提示框容器中
element.appendChild(contentElement);
//在配置项对象中存储提示框容器元素以及关闭按钮元素
option.container = element;
option.close = closeBtn;
//返回提示框容器元素
return element;
}
我们可以看到,我们创建了一个close方法,并传入提示框容器元素,来实现关闭一个提示框,接下来我们来实现这个关闭方法。如下所示:
Message.prototype.close = function (messageElement, time) {
//根据传入的时间来延迟关闭,实际上也就是移除元素
setTimeout(() => {
//判断如果传入了提示框容器元素,并且分两种情况,如果是多个提示框容器元素则循环遍历删除,如果是单个提示框容器元素,则直接删除
if (messageElement && messageElement.length) {
for (let i = 0; i < messageElement.length; i++) {
if (messageElement[i].parentElement) {
messageElement[i].parentElement.removeChild(messageElement[i]);
}
}
} else if (messageElement) {
if (messageElement.parentElement) {
messageElement.parentElement.removeChild(messageElement);
}
}
//关闭了提示框容器元素之后,我们重新设置提示框的top值
this.setTop(document.querySelectorAll('.message'));
}, time * 10);
}
最后我们需要实现的是动态计算消息提示框的top,然后不让消息提示框重叠在一起。代码如下:
Message.prototype.setTop = function (messageElement) {
//这里做一个判断的原因就是当点击页面中最后一个提示框的时候,会重新调用一次,这时获取不到提示框容器元素,所以就不执行后续的设置top
if (!messageElement || !messageElement.length) return;
//由于每个提示框的高度一样,所以我们只需获取第一个提示框元素的高度即可
const height = messageElement[0].offsetHeight;
for (let i = 0; i < messageElement.length; i++) {
//每个提示框的top由一个固定值加上它的高度,并且我们要乘以它的一个索引值
messageElement[i].style.top = (25 * (i + 1) + height * i) + 'px';
}
}
我们想要这样调用$message()
或者$message.info()
,那么我们可以实现如下:
let $message = {};
window['$message'] = $message = function (option) {
let newMessageOption = null;
if (typeof option === 'string') {
newMessageOption = Object.assign(messageOption, { content: option });
} else if (typeof option === 'object' && !!option) {
newMessageOption = Object.assign(messageOption, option);
}
return new Message(newMessageOption);
}
for (let key in typeMap) {
window['$message'][key] = function (option) {
let newMessageOption = null;
if (typeof option === 'string') {
newMessageOption = Object.assign(messageOption, { content: option,type:typeMap[key] });
} else if (typeof option === 'object' && !!option) {
newMessageOption = Object.assign(JSON.parse(JSON.stringify(messageOption)),option,{ type:typeMap[key] });
}
return new Message(newMessageOption);
}
}
整个逻辑也十分简单,无非就是判断传入的配置项,然后进行合并,并传入实例化的Message中。
如此一来,我们就完成了一个消息提示框。
如果以上分析还不懂的话,可以查看我录制的一个视频:
查看原文赞 10 收藏 8 评论 0
夕水 回答了问题 · 2020-05-06
同文件中:
$bgc=#fff
.test
background-color $bgc
不同文件,假定全局变量定义在common.styl
中:
$bgc=#fff
如在index.styl
中使用:
@import 'common.styl'
.test
background-color $bgc
关注 2 回答 1
夕水 回答了问题 · 2020-04-07
你不是已经实现了吗?最多只是代码优化,不要'?'后面的三元表达式即可。
关注 3 回答 3
夕水 赞了回答 · 2020-03-25
你这回答的地方不对,评论里不算回答的
原因写的是超时未回答,如果你是在回答区回答的系统应该不会给这个提示
关注 2 回答 3
推荐关注