Skip to main content

业务

复用组件

进入详情页,使用props将组件和路由解耦:加上 props: true

{
path: '/admin/ecs/:id',
name: 'ecs_detail',
meta: {activeMenu: '/admin/ecs'},
component: () => import('@/admin/ecs/detailPage'),
props: true
}

组件中使用:

export default {
props: ['id'],
methods: {
getId() {
if (this.id) {
this.ecs_id = this.id;
}
}
}
};

还有一种方式可获取路由参数 this.$route.params.id

如果不同路由使用同一个组件需要重新加载这个组件

组件复用时,虽然路由变化了,但是组件并没有被销毁,此时生命周期函数不会被调用,所以视图是不会更新的。

{
path: '/admin/es',
meta: {appId: 'app-WnVLDZGgQ0Mr'},
component: () => import('@/components/AppDeploy')
},
{
path: '/admin/oss',
meta: {appId: 'app-RErkpzXGl4mZ'},
component: () => import('@/components/AppDeploy')
}

方法一、监听$route的变化来初始化数据

watch: {
$route(to) {
if (to.meta.appId) {
this.app_id = to.meta.appId
this.getAppInfo()
}
}
}

方法二、给router-view添加一个唯一的key,可直接设置为路由的完整路径

<router-view :key="$route.fullPath"></router-view>

只要 url 变化了,就一定会重新创建这个组件。

wangEditor 富文本

组件封装

<template>
<div>
<div id="editorElem"></div>
</div>
</template>

<script>
import E from 'wangeditor'
export default {
props: {
value: {
type: String,
default: ''
},
isClear: {
type: Boolean,
default: false
}
},
data() {
return {
editor: null,
editorData: ''
}
},
watch: {
isClear(val) {
if (val) {
this.editor.txt.clear()
this.editorData = null
}
}
},
methods: {
getHtml() {
return this.editor.txt.html()
},

setHtml(val) {
this.editor.txt.html(val)
},

getText() {
this.editor.txt.text()
},

clearVal() {
this.editor.txt.clear()
this.editorData = null
},

initEditor() {
const editor = new E('#editorElem')

editor.config.uploadImgShowBase64 = true // 使用base64保存图片 上下两者不可同用
// this.editor.config.uploadImgServer = "http://baidu.com/"; // 上传图片到服务器

editor.config.uploadImgMaxSize = 2 * 1024 * 1024

/**
// 配置菜单 可根据文档进行添加
this.editor.config.menus = [
// 'head', // 标题
// 'bold', // 粗体
"fontSize", // 字号
// 'fontName', // 字体
// 'italic', // 斜体
"underline", // 下划线
// 'strikeThrough', // 删除线
"foreColor", // 文字颜色
"backColor", // 背景颜色
// 'link', // 插入链接
"list", // 列表
"justify", // 对齐方式
"image", // 插入图片
// 'quote', // 引用
"emoticon" // 表情
// 'table', // 表格
// 'video', // 插入视频
// 'code', // 插入代码
// 'undo', // 撤销
// 'redo', // 重复
// 'fullscreen' // 全屏
];
*/
// 配置 onchange 回调函数,将数据同步到 vue 中
editor.config.onchange = newHtml => {
this.editorData = newHtml
this.$emit('change', this.editorData)
}

editor.create()
this.editor = editor

// this.$nextTick(() => {

// });
}
},
mounted() {
this.initEditor()
},
beforeDestroy() {
this.editor.destroy()
this.editor = null
}
}
</script>

<style scoped lang="less"></style>

使用

<template>
<a-button type="primary" icon="plus" @click="showModal">问题反馈</a-button>
<wangEditor ref="editor" v-model="editorContent" :isClear="isClear" @change="getEditor"></wangEditor>
</template>
<script>
import wangEditor from '@/components/editor/wangeditor';
export default {
components: { wangEditor },
data() {
return {
visible: false,
editorContent: null,
isClear: false
};
},
methods: {
// 获取富文本内容
getEditor(val) {
this.editorContent = val;
},
showModal() {
this.visible = true;
// 打开对话框时都要清空富文本内容
this.$nextTick(() => {
setTimeout(() => {
this.editorContent = '';
this.$refs.editor.setHtml(this.editorContent);
});
});
}
}
};
</script>

轮播缩略图

安装指定版本

npm install swiper@5.3.6 vue-awesome-swiper@4.1.0
<template>
<div class="carousel">
<swiper class="swiper gallery-top" :options="swiperOptionTop" ref="swiperTop">
<swiper-slide v-for="(e, i) in imglist" :key="i">
<img :src="e.url" alt="img" />
</swiper-slide>
<div class="swiper-button-next swiper-button-white" slot="button-next"></div>
<div class="swiper-button-prev swiper-button-white" slot="button-prev"></div>
</swiper>
<swiper class="swiper gallery-thumbs" :options="swiperOptionThumbs" ref="swiperThumbs">
<swiper-slide v-for="(e, i) in imglist" :key="i">
<img :src="e.url" alt="img" />
</swiper-slide>
</swiper>
</div>
</template>

<script>
import { Swiper, SwiperSlide } from 'vue-awesome-swiper';
import 'swiper/css/swiper.css';

export default {
props: {},
components: { Swiper, SwiperSlide },
data() {
return {
imglist: [
{ id: 0, url: require('@/assets/sw/nature-1.jpg') },
{ id: 1, url: require('@/assets/sw/nature-2.jpg') },
{ id: 2, url: require('@/assets/sw/nature-3.jpg') },
{ id: 3, url: require('@/assets/sw/nature-4.jpg') },
{ id: 4, url: require('@/assets/sw/nature-5.jpg') },
{ id: 5, url: require('@/assets/sw/nature-6.jpg') },
{ id: 6, url: require('@/assets/sw/nature-7.jpg') },
{ id: 7, url: require('@/assets/sw/nature-8.jpg') },
{ id: 8, url: require('@/assets/sw/nature-9.jpg') },
{ id: 9, url: require('@/assets/sw/nature-10.jpg') }
],
swiperOptionTop: {
loop: true,
loopedSlides: 5, // looped slides 上下数量要一样
spaceBetween: 10,
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev'
}
},
swiperOptionThumbs: {
loop: true,
loopedSlides: 5,
spaceBetween: 10,
centeredSlides: true,
slidesPerView: 'auto',
touchRatio: 0.2,
slideToClickedSlide: true
}
};
},
mounted() {
this.$nextTick(() => {
const swiperTop = this.$refs.swiperTop.$swiper;
const swiperThumbs = this.$refs.swiperThumbs.$swiper;
swiperTop.controller.control = swiperThumbs;
swiperThumbs.controller.control = swiperTop;
});
}
};
</script>

<style scoped lang="less">
.carousel {
width: 455px;
height: 323px;
img {
width: 100%;
height: 100%;
}

.swiper {
.swiper-slide {
background-size: cover;
background-position: center;
}
&.gallery-top {
height: 80%;
width: 100%;
}
&.gallery-thumbs {
height: 20%;
box-sizing: border-box;
padding: 10px 0;
}
&.gallery-thumbs .swiper-slide {
width: 25%;
height: 100%;
opacity: 0.4;
}
&.gallery-thumbs .swiper-slide-active {
opacity: 1;
}
}
}
</style>

layer 使用方法

LayerVue:http://layer-vue.cn/#/doc,web 弹层窗口

子组件:

<template>
<div>
<LayerVue
:destroyOnClose="true"
:visible.sync="visible"
title="海星湾气象站"
:area="['430px', '530px']"
:offset="['100px', '84px']"
@cancel="cancelShow"
>
<div class="layer16">content</div>
</LayerVue>
</div>
</template>

<script>
export default {
props: { showWeatherMark: Boolean },
data() {
return {
visible: false
};
},
watch: {
showWeatherMark(v) {
if (v) {
this.visible = true;
}
}
},
methods: {
cancelShow() {
this.$emit('changeWeatherMark');
}
}
};
</script>

<style scoped lang="less">
.layer16 {
padding: 16px;
}
</style>

父组件:

<template>
<div>
<a-button type="primary" @click="showWeather">气象站点</a-button>
<weather-site :showWeatherMark="showWeatherMark" @changeWeatherMark="changeWeatherMark"></weather-site>
</div>
</template>

<script>
import WeatherSite from './weatherSite';

export default {
props: { showWeatherMark: Boolean },
components: { WeatherSite },
data() {
return {
showWeatherMark: false
};
},
methods: {
showWeather() {
this.showWeatherMark = true;
},
changeWeatherMark() {
this.showWeatherMark = false;
}
}
};
</script>

g2plot

如果在弹窗中可能会出现 dom 没生成就渲染图表,导致报错,可以使用nextTick,再加一个定时器

<template>
<div style="height:200px" id="container"></div>
</template>
<script>
import { Line } from '@antv/g2plot';
export default {
drawCanview() {
this.$nextTick(() => {
setTimeout(() => {
const line = new Line('container', {
data: lineData,
padding: 'auto',
xField: 'Date',
yField: 'scales',
smooth: true
});
line.render();
}, 500);
});
}
};
</script>

过渡动画

<transition name="chat">
<p v-if="show">hello</p>
</transition>
/* 进入前和结束后的状态 */
.chat-enter,
.chat-leave-to {
opacity: 0;
transform: translateY(80px);
}
/* 进入和离开的动画时间段 */
.chat-enter-active,
.chat-leave-active {
transition: all 0.5s ease;
}

vue-cli

更改系统 title

public/index.html中出现htmlWebpackPlugin.options.title,默认系统 title 显示的是项目名称。如果想更改 title, 除了直接改<title>名称</title>,也可以更改vue.config.js配置webpack

const CONFIG = require('./src/config');

module.exports = {
chainWebpack: config => {
config.plugin('html').tap(args => {
args[0].title = CONFIG.title;
return args;
});
}
};

使用本地字体

在项目的assets文件夹下新建fonts文件夹,将字体文件放在这里,新建font.css

@font-face {
font-family: 'SourceHanSans'; /* 字体名称 */
src: url('./SourceHanSansCN-Normal.otf'); /* 字体路径 */
font-weight: normal;
font-style: normal;
}

然后在main.js里引入import './assets/fonts/font.css',如果要全局用就在App.vue里引入

#app {
font-family: SourceHanSans;
}

国际化

当前主流方案采用 vue-i18n

简化版:将需要翻译的内容放到一个数据源里,切换语言时改变默认语言,传入 key 值返回内容,如下例子

<template>
<button @click="changeLanguage('zh')">中文</button>
<button @click="changeLanguage('en')">英文</button>
<h1>{{ t('msg') }}</h1>
</template>

<script lang="ts" setup>
import { ref } from 'vue';

const locale = {
zh: {
msg: '你好世界'
},
en: {
msg: 'hello world'
}
};
const defaultLocale = ref('zh');
const changeLanguage = (type: string) => {
defaultLocale.value = type;
};
const t = (key: string) => {
return locale[defaultLocale.value][key];
};
</script>

树形组件封装

下拉显示树形结构,可搜索

使用:

<template>
<organize-tree v-model="data" :options="orgOptions"></organize-tree>
</template>

<script>
import OrganizeTree from '@/components/OrganizeTree.vue';

export default {
name: 'Tree',
components: { OrganizeTree },
data() {
return {
data: [],
orgOptions: [] // 树形数据源
};
}
};
</script>

封装

<template>
<div class="root">
<el-select
v-model="selectShowLabel"
:clearable="clearable"
:placeholder="placeholder"
multiple
:collapse-tags="collapseTags"
@clear="clear"
@remove-tag="removeTag"
>
<el-option disabled :style="'margin:5px'" value="">
<el-input
v-model="filterText"
size="small"
prefix-icon="el-icon-search"
clearable
placeholder="输入关键字进行查找"
/>
</el-option>
<el-tree
ref="tree"
:data="options"
show-checkbox
:node-key="defaultProps.value"
:props="defaultProps"
:default-expand-all="expandAll"
:filter-node-method="filterNode"
@check-change="checkChange"
/>
</el-select>
</div>
</template>
<script>
export default {
name: 'OrganizeTree',
model: {
prop: 'checkedArray', // 把父组件传过来的值重命名为checkedArray
event: 'changeChecked' // 把父组件传过来的方法重命名为changeChecked 其实就是 input
},
props: {
// 选中节点的值
checkedArray: { type: Array, default: () => [] },
// 树数据
options: { type: Array, required: true },
// 设置指定的label,value,children
nodeConfig: {
type: Object,
default: () => {
return { label: 'label', value: 'id', children: 'children' };
}
},
// 是否展开所有节点
expandAll: { type: Boolean, default: false },
// 下拉框tag是否折叠
collapseTags: { type: Boolean, default: true },
// 开启下拉框一键清空
clearable: { type: Boolean, default: true },
placeholder: { type: String, default: '请选择' }
},
data() {
return {
timer: null,
selectShowLabel: '', // 用于下拉列表展示
filterText: '' // 筛选输入框绑定值
};
},
computed: {
defaultProps() {
return Object.assign({ label: 'label', value: 'id', children: 'children' }, this.nodeConfig);
}
},
watch: {
// 设置回显
checkedArray: {
handler(val) {
if (val && val.length > 0) {
this.setCheckedNodes(val);
}
},
// 监听第一次数据更改
immediate: true
},
// 筛选符合条件选项
filterText(val) {
this.$refs.tree.filter(val);
}
},
destory() {
clearTimeout(this.timer);
},
methods: {
// 清空树选择的内容
clear() {
this.$refs.tree.setCheckedKeys([]);
},
// select移除选中标签
removeTag(label) {
// 选中项的value
const selectedValueArray = this.getCheckedNodes()
.filter(o => o[this.defaultProps.label] !== label)
.map(o => o[this.defaultProps.value]);
// 移除的节点
const removeNode = this.$refs.tree.getCheckedNodes(true).filter(o => o[this.defaultProps.label] === label);
// 更新树选中节点
removeNode.forEach(o => {
this.$refs.tree.setChecked(o, false, true);
});
// 更新父组件绑定值
this.$emit('changeChecked', selectedValueArray);
},
// 树节点过滤方法
filterNode(value, data) {
if (!value) return true;
return data[this.defaultProps.label].indexOf(value) !== -1;
},
// 获取选中节点
getCheckedNodes() {
return this.$refs.tree.getCheckedNodes(true).map(node => ({
[this.defaultProps.label]: node[this.defaultProps.label],
[this.defaultProps.value]: node[this.defaultProps.value]
}));
},
// 设置选中节点
async setCheckedNodes(selectedArray) {
if (!selectedArray || selectedArray.length === 0) {
this.clear();
return;
}

// 第一次回显dom可能未加载导致setCheckedKeys报错
this.$nextTick(() => {
this.$refs.tree.setCheckedKeys(selectedArray);
this.timer = setTimeout(() => {
this.checkChange();
}, 500);
});
},
// 节点选中状态更改
checkChange() {
// 获取选中的node节点
const selectedArray = this.getCheckedNodes();
// 设置select展示的label
this.selectShowLabel = selectedArray.map(node => node[this.defaultProps.label]);
// 更新model绑定值
const selectValueArray = selectedArray.map(node => node[this.defaultProps.value]);
this.$emit('changeChecked', selectValueArray);
}
}
};
</script>

下拉框单选多选

使用:

<project-search v-model="data" :options="projectSearchOptions" @change="selectChange" />
<template>
<div>
<el-select
v-model="childSelectedValue"
:style="{ width }"
:multiple="multiple"
:collapse-tags="collapseTags"
v-bind="attrs"
v-on="$listeners"
>
<el-checkbox v-if="multiple" v-model="selectChecked" class="all-checkbox" @change="selectAll">全选</el-checkbox>
<el-option v-for="(item, index) in options" :key="index" :label="item[labelKey]" :value="item[valueKey]" />
</el-select>
</div>
</template>
<script>
export default {
name: 'ProjectSearch',
props: {
value: { type: [String, Number, Array] },
multiple: { type: Boolean, default: false }, // 是否多选
collapseTags: { type: Boolean, default: false }, // 选中的标签是否折叠
width: { type: String, default: '100%' }, // 选择框宽度
labelKey: { type: String, default: 'label' }, // 显示项的名称
valueKey: { type: String, default: 'value' }, // 显示项的结果
options: { type: Array, default: () => [] } // 数据
},
computed: {
childSelectedValue: {
get() {
return this.value;
},
set(val) {
this.$emit('input', val);
}
},
attrs() {
return {
// 'popper-append-to-body': false,
clearable: true,
filterable: true,
...this.$attrs
};
},
selectChecked: {
get() {
return this.childSelectedValue?.length === this.options?.length;
},
set(val) {
this.$emit('input', val);
}
}
},
watch: {
childSelectedValue(val) {
this.childSelectedValue = val;
}
},
methods: {
// 点击全选
selectAll(val) {
const options = JSON.parse(JSON.stringify(this.options));
if (val) {
this.childSelectedValue = options.map(item => {
return item[this.valueKey];
});
} else {
this.childSelectedValue = null;
}
}
}
};
</script>
<style lang="scss">
.el-select-dropdown {
.all-checkbox {
margin-left: 20px;
}
}
</style>

svg-sprite-loader

场景:在项目中方便地引入 svg 图标

原理:利用 svg 的 symbol 元素,将每个 icon 包括在 symbol 中,通过 use 元素使用该 symbol

npm i -D svg-sprite-loader

自动导入

src/assets/icons目录,创建 index.js 和 svg 文件夹,svg 目录下放 svg 图标

index.js

import Vue from 'vue';
import SvgIcon from '@/components/SvgIcon'; // svg组件

// 全局注册
Vue.component('svg-icon', SvgIcon);

// 指定目录及遍历的规则
// 接受三个参数,第一个是文件夹,第二个是是否使用子文件,第三个是文件匹配的正则
const req = require.context('./svg', false, /\.svg$/);

// 遍历
const requireAll = requireContext => requireContext.keys().map(requireContext);

// webpack自动导入所有svg
requireAll(req);

main.js

import './assets/icons';

配置 webpack loader

vue.config.js

module.exports = {
chainWebpack(config) {
config.module.rule('svg').exclude.add(resolve('src/assets/icons')).end();
config.module
.rule('icons')
.test(/\.svg$/)
.include.add(resolve('src/assets/icons'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]'
})
.end();
}
};

组件调用

SvgIcon.vue

<template>
<svg :class="svgClass" aria-hidden="true">
<use :xlink:href="iconName" />
</svg>
</template>

<script>
export default {
name: 'SvgIcon',
props: {
iconClass: { type: String, required: true },
className: { type: String, default: '' }
},
computed: {
iconName() {
return `#icon-${this.iconClass}`;
},
svgClass() {
return 'svg-icon' + (this.className || '');
}
}
};
</script>

<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>

在组件中使用

<svg-icon icon-class="search" />

在 vue-cli 中引入可选链操作符

在 vue-cli 创建的项目中引入可选链、空值合并操作符

npm i -D @babel/plugin-proposal-optional-chaining  @babel/plugin-proposal-nullish-coalescing-operator

配置babel.config.js

module.exports = {
plugins: ['@babel/plugin-proposal-optional-chaining', '@babel/plugin-proposal-nullish-coalescing-operator']
};

使用:a?.b?.ca??b

注意:目前 Vue 默认是不支持在 template 中使用可选链操作符的

拖拽、缩放

<template>
<div class="screen">
<div ref="screenBody" class="screen-body">
<div ref="screenTable" class="screen-table">666</div>
</div>
</div>
</template>

<script>
export default {
data() {
return {
scaleFloor: 1,
scaleStep: 0.05,
preDownPos: {}
};
},
mounted() {
this.$nextTick(() => {
const docScreen = this.$refs.screenBody;
docScreen.addEventListener('mousewheel', this.scrollFunc);
docScreen.addEventListener('DOMMouseScroll', this.scrollFunc);
docScreen.addEventListener('mousedown', this.downFunc);
docScreen.addEventListener('mouseup', this.upFunc);
});
},
methods: {
// 拖拽、缩放
scrollFunc(e) {
e = e || window.event;
if (e.wheelDelta) {
// IE/Opera/Chrome
this.mouseScroll(e.wheelDelta);
} else if (e.detail) {
// Firefox
this.mouseScroll(-e.detail);
}
},
downFunc(event) {
document.addEventListener('mousemove', this.moveFunc);
this.preDownPos = this.getMousePos(event);
},
moveFunc(event) {
const mosPostion = this.getMousePos(event);
this.moveScreenTable(mosPostion.x - this.preDownPos.x, mosPostion.y - this.preDownPos.y);
this.preDownPos = this.getMousePos(event);
},
upFunc() {
document.removeEventListener('mousemove', this.moveFunc);
},
moveScreenTable(x, y) {
const docScreen = this.$refs.screenTable;
let moveX = this.getPixelValue(docScreen, 'left');
let moveY = this.getPixelValue(docScreen, 'top');
moveX += x;
moveY += y;
docScreen.style.left = moveX + 'px';
docScreen.style.top = moveY + 'px';
},
mouseScroll(step) {
if (step > 0) {
this.scaleFloor += this.scaleStep;
} else if (step < 0 && this.scaleFloor > this.scaleStep * 2) {
this.scaleFloor -= this.scaleStep;
}
if (this.scaleFloor > 0) {
this.$refs.screenTable.style.transform = `scale(${this.scaleFloor})`;
} else {
this.$refs.screenTable.style.transform = '';
}
},
getMousePos(event) {
const e = event || window.event;
const x = e.clientX - this.$refs.screenBody.offsetLeft;
const y = e.clientY - this.$refs.screenBody.offsetTop;
return { x: x, y: y };
},
getPixelValue(doc, key) {
return Number(window.getComputedStyle(doc)[key].replace('px', ''));
}
}
};
</script>

<style lang="scss" scoped>
.screen {
height: calc(100vh - 86px);
display: flex;
.screen-body {
position: relative;
width: 100%;
overflow: hidden;
.screen-table {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
}
}
</style>