在vue项目中,为了开发便利和可复用性的考虑。经常会封装一些比较常用的组件, 随着能力和水平的提高,封装的组件的功能会越来越多,内容也越来越多。 所以我觉得需要把这些常用的公共组件都提炼出来, 在今后再做项目或者review代码的时候都可以随时去进行查漏补缺和快速实现功能。 针对每个项目都重新写一套组件即费时,也可能仓促写的容易出现bug。 所以我打算在今后的工作中, 把自己认为封装的好一些的组件都记录在这篇博客里, 组件里的一些bug也会不断的修复, 功能也会不断扩展。 争取能做到一次封装, 多处复用, 且易于阅读,扩展性高。
1. 如何引用公共组件
组件可以通过局部引入和全局引入。
- 局部引入的步骤, 首先在需要引入的文件中import组件, 然后在components对象中注入, 最后在template中使用。
import partComponent from './partCompnent'
components: {partComponent}
<part-component></part-component>
- 全局引入的步骤。 我比较喜欢, 新建一个文件夹,例
importGlobalComp
, 然后使用一个js方法去遍历这个文件夹下的所有.vue
文件, 全局注册到vue中。 importGlobalComp.js
// 全局注册全局组件
import Vue from 'vue';
const requireComponent = require.context(
// 其组件目录的相对路径
// 这块是存放公共组件的文件目录, 是需要根据相应的路径更改的
'@/components',
// 是否查询其子目录
true,
// 匹配基础组件文件名的正则表达式
/\.vue$/
);
requireComponent.keys().forEach((fileName) => {
// 获取组件配置
const componentConfig = requireComponent(fileName);
// 获取组件的 PascalCase 命名
let componentName =
// 获取和目录深度无关的文件名
fileName.replace(/^\.\//, '').replace(/\.\w+$/, '');
// 递归遍历文件下的.vue文件, 获取其文件名。
let reverseName = componentName.split('').reverse().join('')
componentName = reverseName.split('/')[0].split('').reverse().join('');
// 全局注册组件
Vue.component(
componentName,
// 如果这个组件选项是通过 `export default` 导出的,
// 那么就会优先使用 `.default`,
// 否则回退到使用模块的根。
componentConfig.default || componentConfig
);
});
main.js
// main.js中引入js文件
import '@/utils/importGlobalComp'
2. element-ui相关的二次封装组件
1. 对elment-ui的单日期组件 el-date-picker 进行的二次封装(element-ui)
- 父组件使用方法
<g-date :val.sync="initDate" title="请选择单日期"></g-date>
解释:
:val.sync
是子传父方法$emit
的语法糖, 传值方法相应的改为this.$emit("update:val", v)
这种方式。 这样就不用再使用子传父后, 父通过方法去更新值的操作了。这样不仅省去了子传父数据更新的操作, 更重要的是利于组件的复用。 我们只需要调用组件即可实现数据的双向数据绑定, 不必再另写代码去处理组件内部的逻辑。
- 子组件的gDate.vue文件代码
<!--
* @描述
* element的单日期组件。 可传默认值或者不传。
* title可传可不传, initDate中传递的是字符串日期或者 new Date()格式的日期
* @使用方法
* <g-date :val.sync="initDate" title="请选择单日期" />
* @initDate可选你参数
* new Date() "2020-03-02" "2020/03/02" ""
* @LastEditTime: 最后更新时间
* 2021-07-20
-->
<template>
<div class="single_date">
<div>
<span
class="title"
v-if="title"
>{{title}}:</span>
<el-date-picker
v-model="timePicker"
value-format="yyyy-MM-dd HH:mm:ss"
type="date"
@change="dateChange"
:picker-options="pickerOptions"
placeholder="选择日期"
></el-date-picker>
</div>
</div>
</template>
<script>
export default {
name: "GDate",
props: {
val: {
type: [String, Date],
default: ""
},
title: {
type: String,
default: ""
}
},
watch: {
val(e) {
this.timePicker = e;
}
},
data() {
return {
timePicker: this.val,
pickerOptions: {
disabledDate(time) {
return time.getTime() > Date.now();
},
shortcuts: [
{
text: "今天",
onClick(picker) {
picker.$emit("pick", new Date());
}
},
{
text: "昨天",
onClick(picker) {
const date = new Date();
date.setTime(date.getTime() - 3600 * 1000 * 24);
picker.$emit("pick", date);
}
},
{
text: "一周前",
onClick(picker) {
const date = new Date();
date.setTime(date.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit("pick", date);
}
}
]
}
};
},
methods: {
dateChange(v) {
console.log('11111<<< v >>>11111');
console.log(v);
this.timePicker = v;
this.$emit("update:val", v);
}
}
};
</script>
<style scoped lang="scss">
.title {
margin-right: 10px;
}
</style>
2. 对对elment-ui的双日期组件 el-date-picker 进行的二次封装(element-ui)
- 父组件使用方法
<g-double-date :val.sync="doubleDate" title="请选择日期范围"></g-double-date>
解释: :val.sync
是子传父方法$emit的语法糖。 不必再进行子传父后, 父通过方法去更新date值的操作了。 title可写可不写。
可传值:: new Date()
和 2020-10-24 03:02:03
这种类型的数据。 如果想传递年月日, 修改value-format="yyyy-MM-dd"
- 子组件的gDoubleDate.vue文件代码
<!--
* @描述
* element的双日期组件。 可传默认值或者不传。
* @使用方法
* <g-double-date :val.sync="doubleDate" title="请选择日期范围" />
* @doubleDate可选你参数
* "" [new Date(+new Date() - 24 * 60 * 60 * 1000 * 3), new Date()] [new Date(2000, 10, 10), new Date(2000, 10, 13)]
* @LastEditTime: 最后更新时间
* 2021-07-20
-->
<template>
<div class="double_date">
<div>
<span
class="title"
v-if="title"
>{{title}}:</span>
<el-date-picker
v-model="timePicker"
type="daterange"
align="right"
unlink-panels
format="yyyy-MM-dd"
value-format="yyyy-MM-dd HH:mm:ss"
start-placeholder="开始日期"
range-separator="~"
end-placeholder="结束日期"
:picker-options="pickerOptions"
@change="dateChange"
></el-date-picker>
</div>
</div>
</template>
<script>
import moment from "moment";
export default {
name: "GDoubleDate",
props: {
val: {
default: ""
},
title: {
default: ""
}
},
watch: {
val(e) {
this.timePicker = e;
}
},
data() {
return {
timePicker: this.val,
pickerOptions: {
// 时间选择器时间段
shortcuts: [
{
text: "本月",
onClick(picker) {
let start = moment(
moment()
.month(moment().month())
.startOf("month")
.valueOf()
)._d;
let end = moment(
moment()
.month(moment().month())
.endOf("month")
.valueOf()
)._d;
picker.$emit("pick", [start, new Date()]);
}
},
{
text: "最近一周",
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit("pick", [start, end]);
}
},
{
text: "最近一个月",
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
picker.$emit("pick", [start, end]);
}
},
{
text: "最近三个月",
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
picker.$emit("pick", [start, end]);
}
}
]
}
};
},
methods: {
dateChange(v) {
console.log('11111<<< v >>>11111');
console.log(v);
this.timePicker = v;
this.$emit("update:val", v);
}
}
};
</script>
<style scoped lang="scss" >
.title {
margin-right: 10px;
}
</style>
3. 点击回到顶部gBackToTop.vue
使用: <g-back-to-top></g-back-to-top>
<template>
<el-tooltip
placement="top"
content="回到顶部"
>
<transition :name="transitionName">
<div
v-show="visible"
:style="customStyle"
class="back-to-ceiling"
@click="backToTop"
>
<svg
width="16"
height="16"
viewBox="0 0 17 17"
xmlns="http://www.w3.org/2000/svg"
class="Icon Icon--backToTopArrow"
aria-hidden="true"
style="height:16px;width:16px"
>
<path d="M12.036 15.59a1 1 0 0 1-.997.995H5.032a.996.996 0 0 1-.997-.996V8.584H1.03c-1.1 0-1.36-.633-.578-1.416L7.33.29a1.003 1.003 0 0 1 1.412 0l6.878 6.88c.782.78.523 1.415-.58 1.415h-3.004v7.004z" /></svg>
</div>
</transition>
</el-tooltip>
</template>
<script>
export default {
name: 'BackToTop',
props: {
visibilityHeight: {
type: Number,
default: 400
},
backPosition: {
type: Number,
default: 0
},
customStyle: {
type: Object,
default: () => {
return {
right: '50px',
bottom: '50px',
width: '40px',
height: '40px',
zIndex: '10',
opacity: 0.7,
'border-radius': '4px',
'line-height': '45px',
background: '#e7eaf1'
}
}
},
transitionName: {
type: String,
default: 'fade'
}
},
data() {
return {
visible: false,
interval: null,
isMoving: false
}
},
mounted() {
window.addEventListener('scroll', this.handleScroll)
},
beforeDestroy() {
window.removeEventListener('scroll', this.handleScroll)
if (this.interval) {
clearInterval(this.interval)
}
},
methods: {
handleScroll() {
this.visible = window.pageYOffset > this.visibilityHeight
},
backToTop() {
if (this.isMoving) return
const start = window.pageYOffset
let i = 0
this.isMoving = true
this.interval = setInterval(() => {
const next = Math.floor(this.easeInOutQuad(10 * i, start, -start, 500))
if (next <= this.backPosition) {
window.scrollTo(0, this.backPosition)
clearInterval(this.interval)
this.isMoving = false
} else {
window.scrollTo(0, next)
}
i++
}, 3.7)
},
easeInOutQuad(t, b, c, d) {
if ((t /= d / 2) < 1) return c / 2 * t * t + b
return -c / 2 * (--t * (t - 2) - 1) + b
}
}
}
</script>
<style scoped>
.back-to-ceiling {
position: fixed;
display: inline-block;
text-align: center;
cursor: pointer;
}
.back-to-ceiling:hover {
background: #d5dbe7;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.back-to-ceiling .Icon {
fill: #9aaabf;
background: none;
}
</style>
4. 可拖拽的dialog。
原理: 在el-dialog中引入directive的自定义指令实现可拖拽。 使用sync的语法糖直接控制弹出框的显隐。
// 在vue文件中使用
<g-dialog
:showDialog.sync="isShow"
title="弹出框卡号"
@confirm="confirmDialog"
></g-dialog>
公共组件代码
<template>
<div>
<el-dialog
:modal="true"
:modal-append-to-body="true"
:title="title"
v-el-drag-dialog
:width="width"
:height="height"
:visible.sync="isShow"
:close-on-click-modal="false"
:show-close="true"
:close-on-press-escape="true"
@close="cancelHandle"
>
<slot></slot>
<span
slot="footer"
class="dialog-footer"
>
<el-button @click="cancelHandle">取 消</el-button>
<el-button
type="primary"
@click="confirmHandle"
v-throttle
>确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'GDialog',
props: {
title: {
type: String,
default: '标题'
},
showDialog: {
type: Boolean,
default: false
},
width: {
type: String,
default: "45%"
},
height: {
type: String,
default: "60%"
},
cancel: {
type: [Function, String],
default: ''
}
},
data() {
return {
isShow: this.showDialog
};
},
watch: {
showDialog() {
this.isShow = this.showDialog;
}
},
created() {
},
mounted() {
},
methods: {
confirmHandle() {
this.$emit('confirm');
},
cancelHandle() {
if (typeof this.cancel === 'function') {
this.cancel();
} else {
this.$emit("update:showDialog", false);
}
}
}
};
</script>
<style scoped lang='scss'>
</style>
// 可拖拽的directive代码 el-drag-dialog
Vue.directive('el-drag-dialog', {
bind(el, binding, vnode) {
const dialogHeaderEl = el.querySelector('.el-dialog__header')
const dragDom = el.querySelector('.el-dialog')
dialogHeaderEl.style.cssText += ';cursor:move;'
dragDom.style.cssText += ';top:0px;'
// 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
const getStyle = (function() {
if (window.document.currentStyle) {
return (dom, attr) => dom.currentStyle[attr]
} else {
return (dom, attr) => getComputedStyle(dom, false)[attr]
}
})()
dialogHeaderEl.onmousedown = (e) => {
// 鼠标按下,计算当前元素距离可视区的距离
const disX = e.clientX - dialogHeaderEl.offsetLeft
const disY = e.clientY - dialogHeaderEl.offsetTop
const dragDomWidth = dragDom.offsetWidth
const dragDomHeight = dragDom.offsetHeight
const screenWidth = document.body.clientWidth
const screenHeight = document.body.clientHeight
const minDragDomLeft = dragDom.offsetLeft
const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth
const minDragDomTop = dragDom.offsetTop
const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight
// 获取到的值带px 正则匹配替换
let styL = getStyle(dragDom, 'left')
let styT = getStyle(dragDom, 'top')
if (styL.includes('%')) {
styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100)
styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100)
} else {
styL = +styL.replace(/\px/g, '')
styT = +styT.replace(/\px/g, '')
}
document.onmousemove = function(e) {
// 通过事件委托,计算移动的距离
let left = e.clientX - disX
let top = e.clientY - disY
// 边界处理
if (-(left) > minDragDomLeft) {
left = -minDragDomLeft
} else if (left > maxDragDomLeft) {
left = maxDragDomLeft
}
if (-(top) > minDragDomTop) {
top = -minDragDomTop
} else if (top > maxDragDomTop) {
top = maxDragDomTop
}
// 移动当前元素
dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`
// emit onDrag event
vnode.child.$emit('dragDialog')
}
document.onmouseup = function(e) {
document.onmousemove = null
document.onmouseup = null
}
}
}
})
5. 远程可搜索的select
<template>
<div class="select_box">
<el-select
v-bind="$attrs"
v-on="$listeners"
:multiple="true"
v-model="sValue"
@change="changeVal"
filterable
remote
reserve-keyword
placeholder="请输入关键词远程搜索"
:remote-method="remoteMethod"
>
<el-option
v-for="item in tableData"
:key="item[sendValue]"
:label="item[sendLabel]"
:value="item[sendValue]"
>
</el-option>
</el-select>
</div>
</template>
<script>
/**
* @描述
* 远程搜索input
* @使用方法
<GRemoteSearch
ref="remoteSearchRef"
sendLabel="realName"
sendValue="id"
sendName="name"
:sendList="form.value"
type="cusUserList"
></GRemoteSearch>
* @param
*
* @LastEditTime: 最后更新时间
* 2022-04-01
* @Author: andy凌云
*/
import { cusUserList } from '@/server/plateformApi' // 这是用户列表
export default {
props: {
sendLabel: {
type: String,
default: "label",
},
// 取名id会报错
sendValue: {
type: String,
default: "id",
},
sendName: {
type: String,
default: 'name'
},
// 这里传递的是接口名称
sendType: {
type: String,
default: "cusUserList",
},
sendList: {
type: Array,
default: () => [],
},
},
data() {
return {
// tableData: [],
tableData: [],
sValue: [],
saveData: [],
isSelect: false,
}
},
watch: {
sendList(val) {
this._handleValue(val)
},
sValue(newVal, oldVal) {
if (!this.isSelect) {
return
}
let differenceArr = newVal.concat(oldVal).filter(v => !newVal.includes(v) || !oldVal.includes(v))
let difference = differenceArr[0]
// 新增
if (newVal.length > oldVal.length) {
this.tableData.forEach(v => {
if (v[this.sendValue] === difference) {
let obj = {
[this.sendValue]: v[this.sendValue],
[this.sendLabel]: v[this.sendLabel],
[this.sendName]: v[this.sendName],
}
this.saveData.push(obj);
}
})
} else {
// 减少
let findIdx = this.saveData.findIndex(v => v[this.sendValue] === difference)
this.saveData.splice(findIdx, 1);
}
console.log(`obj打印***** this.saveData ***** 107行 ~/kj/gaea-fe/src/components/autoImportComps/testComps/gRemoteSearch.vue 16:22:24`);
console.log(JSON.stringify(this.saveData, null, '\t'));
}
},
created() {
// this.sValue = this._handleValue(this.sendList);
this._handleValue(this.sendList);
},
mounted() {
},
methods: {
// 比较前后的值并生成add和delete的数组对象
_compareValueChange() {
// 获取两个数组的差集
let toFaterArr = [];
this.saveData.forEach((v, i) => {
// 如果传递进来的sendList的长度为0, 那就全是add
if (this.sendList.length === 0) {
toFaterArr.push(this.changeValueType(v));
} else {
// 如果saveData中的值sendList不存在, 则为 ADD
let isHas = this.sendList.find(val => {
return val.data[this.sendValue] === v[this.sendValue]
})
if (!isHas) {
toFaterArr.push(this.changeValueType(v));
}
}
})
// 遍历父组件传递过来的list。 如果在saveList中找不到就是删除。
this.sendList.forEach(v => {
let isHas = this.saveData.find(item => {
return item[this.sendValue] === v.data[this.sendValue];
})
if (!isHas) {
toFaterArr.push(this.changeValueType(v, 'DELETE'));
}
})
return toFaterArr;
},
changeValueType(item, type = 'ADD') {
console.log(`obj打印***** item ***** 149行 ~/kj/gaea-fe/src/components/autoImportComps/testComps/gRemoteSearch.vue 16:47:36`);
console.log(JSON.stringify(item, null, '\t'));
let obj = {
data: {
[this.sendValue]: type === 'ADD' ? item[this.sendValue] : item.data[this.sendValue],
[this.sendName]: type === 'ADD' ? item[this.sendName] : item.data[this.sendName],
[this.sendLabel]: type === 'ADD' ? item[this.sendLabel] : item.data[this.sendLabel],
},
opType: type,
}
return obj;
},
changeVal(val1, val2) {
this.isSelect = true;
},
_handleValue(val) {
if (val.length === 0) {
this.sValue = []
this.tableData = [];
return
}
let copyList = this.$pub.deepClone(val);
this.sValue = [];
copyList.forEach(v => {
this.sValue.push(v.data.id);
let obj = {}
obj[this.sendLabel] = v.data[this.sendLabel]
obj[this.sendValue] = v.data[this.sendValue]
obj[this.sendName] = v.data[this.sendName];
this.tableData.push(obj);
this.saveData = this.$pub.deepClone(this.tableData);
})
},
async remoteMethod(query) {
if (query !== '') {
let sendParams = {
size: 100,
"queries": [
{
"queryType": "like",
"field": "word",
"queryData": query
}
],
"current": 1,
}
let reqUrl = ''
if (this.sendType === 'cusUserList') {
reqUrl = cusUserList
}
this.$pub.initPage(this, sendParams, reqUrl)
} else {
this.tableData = [];
}
}
}
}
</script>
<style lang="scss" scoped>
.select_box ::v-deep .el-select {
width: 400px !important;
}
</style>